forked from Cookies_Github_mirror/AquaDX
Compare commits
619 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85296ae3b1 | ||
|
|
8e882aafa1 | ||
|
|
25edbf06c7 | ||
|
|
4f05365da3 | ||
|
|
bf7de99524 | ||
|
|
08c27b6c58 | ||
|
|
60661757c6 | ||
|
|
5ba64483fb | ||
|
|
a30c9391eb | ||
|
|
7023e726bd | ||
|
|
c616ea81c6 | ||
|
|
65f8b587af | ||
|
|
14f6b9c759 | ||
|
|
c83e0f8cff | ||
|
|
2bf86423c9 | ||
|
|
8dc3035b66 | ||
|
|
0aff0330e7 | ||
|
|
41852f2467 | ||
|
|
442ec76828 | ||
|
|
d8fc14e71b | ||
|
|
2630d32764 | ||
|
|
74d7eff577 | ||
|
|
355c9e2a3d | ||
|
|
501bf06ada | ||
|
|
4574bc0b2f | ||
|
|
c6c91b84fe | ||
|
|
066b33e3e8 | ||
|
|
15002c45d6 | ||
|
|
b41f3b9370 | ||
|
|
02e2700e96 | ||
|
|
6441dfd219 | ||
|
|
1e229c12cc | ||
|
|
4219f2db5b | ||
|
|
36ce636093 | ||
|
|
47f09f81ff | ||
|
|
bfa6df904d | ||
|
|
99d4f55c50 | ||
|
|
7728b4b1ab | ||
|
|
6a475434ad | ||
|
|
876a0bd108 | ||
|
|
ba13bfd9ad | ||
|
|
44bab8c0c7 | ||
|
|
2d229b82c3 | ||
|
|
c6cce7aa9a | ||
|
|
5cbf09f24e | ||
|
|
3ca7d3d615 | ||
|
|
25840be694 | ||
|
|
34ab608425 | ||
|
|
b498160b3a | ||
|
|
23aae3b5b9 | ||
|
|
97fdd096a8 | ||
|
|
0d21a02da9 | ||
|
|
ab94250b05 | ||
|
|
42ca6f79dc | ||
|
|
646795b753 | ||
|
|
de649915e2 | ||
|
|
0093f5a0de | ||
|
|
686b50eeda | ||
|
|
0c93b85024 | ||
|
|
49d4e88022 | ||
|
|
3a8616e225 | ||
|
|
d4178c85a9 | ||
|
|
de46790bdf | ||
|
|
c27070ae28 | ||
|
|
bb4c9477da | ||
|
|
95e78e4f93 | ||
|
|
d3d7b5a5c7 | ||
|
|
45a3d74284 | ||
|
|
cd972b5c61 | ||
|
|
341be8bdc1 | ||
|
|
101c24edc5 | ||
|
|
be34915cdf | ||
|
|
70aed1d5db | ||
|
|
d8c1144881 | ||
|
|
68ec7f504a | ||
|
|
3ab2b16042 | ||
|
|
d7fc6f9f49 | ||
|
|
26a72244c0 | ||
|
|
abc21badb1 | ||
|
|
aa1caacfd6 | ||
|
|
3663eb63e7 | ||
|
|
e885700680 | ||
|
|
d6170d602a | ||
|
|
4dbb287e11 | ||
|
|
e537e0f115 | ||
|
|
3613d7a37b | ||
|
|
b5e98f505f | ||
|
|
3dc9ca6822 | ||
|
|
e13ddeaaad | ||
|
|
56ce7f9696 | ||
|
|
4ebddf78ed | ||
|
|
2682165da8 | ||
|
|
373e7dc8ad | ||
|
|
0551f8bff1 | ||
|
|
b4454cc812 | ||
|
|
40fb1c8868 | ||
|
|
f97cb4a1bb | ||
|
|
56d0786702 | ||
|
|
d880ecd709 | ||
|
|
1bee9e19e6 | ||
|
|
c5879ae5a7 | ||
|
|
64f458e15a | ||
|
|
2fa5d09fc9 | ||
|
|
d6fc60e02b | ||
|
|
bb9bfd6396 | ||
|
|
0fbe139e8d | ||
|
|
571591f021 | ||
|
|
8a1d2383b8 | ||
|
|
00c5edcea7 | ||
|
|
39d62099df | ||
|
|
c5d6f6f5b9 | ||
|
|
13f3cf1e90 | ||
|
|
93f6bf8ba3 | ||
|
|
1cdbed51cd | ||
|
|
50ae04bb4e | ||
|
|
a55d503faa | ||
|
|
7fc4f83eb5 | ||
|
|
bc831b4d30 | ||
|
|
c6190146aa | ||
|
|
3f01152a4a | ||
|
|
ad5c652a8f | ||
|
|
9609db941b | ||
|
|
bbb8447f5c | ||
|
|
22ca06af3e | ||
|
|
af11758190 | ||
|
|
32fcc25ea4 | ||
|
|
b3fcf8dd5e | ||
|
|
b7d2a97f05 | ||
|
|
ad13875137 | ||
|
|
e14a131480 | ||
|
|
64ba0db228 | ||
|
|
c99d8e7e75 | ||
|
|
305d1cea94 | ||
|
|
f314b3982e | ||
|
|
5ea2615b93 | ||
|
|
17123fec35 | ||
|
|
73d05e7cbf | ||
|
|
3380ea3609 | ||
|
|
101527d3e1 | ||
|
|
df9ab3250c | ||
|
|
d533df52de | ||
|
|
d2cf16d046 | ||
|
|
40a65b5e13 | ||
|
|
fa33cb680e | ||
|
|
2757eb91ce | ||
|
|
2842429ced | ||
|
|
fb2a26c5b7 | ||
|
|
cab1dc8838 | ||
|
|
0ec76dcde3 | ||
|
|
c41046953e | ||
|
|
30f740a430 | ||
|
|
1e8c0ce99b | ||
|
|
aa3a3d9181 | ||
|
|
6d0f528201 | ||
|
|
131cd5915c | ||
|
|
f5512fa162 | ||
|
|
484bb758ae | ||
|
|
89461893a4 | ||
|
|
54e865feb2 | ||
|
|
015fa3dc9f | ||
|
|
cf015be49f | ||
|
|
b6c8993f7e | ||
|
|
1ef37d91e8 | ||
|
|
7fc81cf363 | ||
|
|
123bf9de34 | ||
|
|
d3f6b75d34 | ||
|
|
a5fe5f53e2 | ||
|
|
e91029f66e | ||
|
|
2a7ce54c28 | ||
|
|
f3b2d4dc57 | ||
|
|
95b9871f7f | ||
|
|
533af83749 | ||
|
|
e4330fee92 | ||
|
|
5fec57e8e3 | ||
|
|
95a06d572b | ||
|
|
cc8c125934 | ||
|
|
91c605ee4b | ||
|
|
f44fe4def1 | ||
|
|
7c0a1ea089 | ||
|
|
ce5c4d1111 | ||
|
|
98952972a0 | ||
|
|
f728b6ab1b | ||
|
|
e799b48877 | ||
|
|
fc8ecb7470 | ||
|
|
ac18234e29 | ||
|
|
59b17aa47e | ||
|
|
9155bfb886 | ||
|
|
cbe683d25e | ||
|
|
64f057a415 | ||
|
|
3da308346e | ||
|
|
313dd681de | ||
|
|
aaf7e1e3e5 | ||
|
|
9f831fd8b5 | ||
|
|
450397481e | ||
|
|
6fb8978f48 | ||
|
|
4a7bf4b31e | ||
|
|
38e94210e4 | ||
|
|
d338809750 | ||
|
|
7fd7e17d1d | ||
|
|
a5a5bd80c4 | ||
|
|
d264ca1ed4 | ||
|
|
85dd19509c | ||
|
|
faf1945933 | ||
|
|
3c6d6ff702 | ||
|
|
c6ecc89ad3 | ||
|
|
abed79441d | ||
|
|
906199a517 | ||
|
|
6f34c21d94 | ||
|
|
9ba1a68b51 | ||
|
|
073c72fd63 | ||
|
|
3ac4af1558 | ||
|
|
5057f6848f | ||
|
|
b3955731c2 | ||
|
|
af83cf552e | ||
|
|
a0426044e8 | ||
|
|
432635d567 | ||
|
|
02b78320ec | ||
|
|
f1461f905d | ||
|
|
e1cdb3ab65 | ||
|
|
6218424be3 | ||
|
|
5a9b7e296f | ||
|
|
f4cc9c7734 | ||
|
|
e0c7998448 | ||
|
|
752d65557f | ||
|
|
a952674df7 | ||
|
|
25f5f6e1f7 | ||
|
|
0f1d6c0984 | ||
|
|
8dd4bb9d61 | ||
|
|
98275ade59 | ||
|
|
95cc9f0e21 | ||
|
|
742ea50c2c | ||
|
|
54b1174e1b | ||
|
|
e07de72fa4 | ||
|
|
13b4af3734 | ||
|
|
29566a6c93 | ||
|
|
7669f7d9a0 | ||
|
|
5913d5b585 | ||
|
|
d9a332de44 | ||
|
|
e85533686e | ||
|
|
0100140dc0 | ||
|
|
8def9e8931 | ||
|
|
6fc2f26983 | ||
|
|
ed1ed6cbe9 | ||
|
|
10d19a5392 | ||
|
|
7bbd90ab91 | ||
|
|
9565d48b04 | ||
|
|
284d366b44 | ||
|
|
a9893379f4 | ||
|
|
50677ad81d | ||
|
|
71d7fcbe65 | ||
|
|
8342acbd49 | ||
|
|
d5296763ad | ||
|
|
73efa4fe91 | ||
|
|
82f573e1a1 | ||
|
|
4ef0ac3fee | ||
|
|
bc246f39d2 | ||
|
|
f9af23dbca | ||
|
|
68f8ef0b24 | ||
|
|
16f6acf8fc | ||
|
|
3faa5b2f52 | ||
|
|
04a7c068f4 | ||
|
|
92dee27634 | ||
|
|
7dda25f96b | ||
|
|
40f700910a | ||
|
|
aa90b34511 | ||
|
|
45cf082bb9 | ||
|
|
0ab78983d4 | ||
|
|
e137210cbc | ||
|
|
3093755c9e | ||
|
|
94c4950d23 | ||
|
|
fa0a624b7c | ||
|
|
f3fabe1708 | ||
|
|
52ec890e2c | ||
|
|
2a10471e0b | ||
|
|
94c1974d2f | ||
|
|
f0a8014efb | ||
|
|
96cac6ca68 | ||
|
|
0da50bc693 | ||
|
|
1169ac44b4 | ||
|
|
38367279ff | ||
|
|
ef00cfbddd | ||
|
|
e514e4b64e | ||
|
|
f1af07e921 | ||
|
|
44cf022e70 | ||
|
|
7e68de5a17 | ||
|
|
51f73d77bf | ||
|
|
fa4ccf07b8 | ||
|
|
49da7aafd0 | ||
|
|
58ca71baaa | ||
|
|
2c550a0874 | ||
|
|
dcb671acd8 | ||
|
|
56600d3f27 | ||
|
|
6913f7bdf5 | ||
|
|
bcc2d286ed | ||
|
|
aed6c2123f | ||
|
|
68626fecd7 | ||
|
|
441d7376cb | ||
|
|
c9ac38de01 | ||
|
|
b9c063c41e | ||
|
|
55804be70e | ||
|
|
2b749af917 | ||
|
|
9378dfdd04 | ||
|
|
46768c77b7 | ||
|
|
ff9358b986 | ||
|
|
f3090870be | ||
|
|
666fbe8ce7 | ||
|
|
67b29851ea | ||
|
|
1a2cd201a7 | ||
|
|
5041bf67a5 | ||
|
|
1a2f3bf80e | ||
|
|
d7a231eb18 | ||
|
|
21c9c190aa | ||
|
|
a781c2d665 | ||
|
|
09c3ce3164 | ||
|
|
a7888a63fa | ||
|
|
79dd56d017 | ||
|
|
882d04f50c | ||
|
|
a7fd414ce6 | ||
|
|
eb30451cfa | ||
|
|
a48f2b1f17 | ||
|
|
d3665d64a6 | ||
|
|
a4bbc9c3c6 | ||
|
|
b0ffda42bc | ||
|
|
b333045d41 | ||
|
|
ef5d0a81eb | ||
|
|
48819c10a9 | ||
|
|
9ae23e4395 | ||
|
|
4d36efebb7 | ||
|
|
e842a37654 | ||
|
|
c821626dc1 | ||
|
|
16aba9ff96 | ||
|
|
a11bfdb13b | ||
|
|
c0437e55eb | ||
|
|
279bcbfeab | ||
|
|
6555263496 | ||
|
|
8db4e17a8a | ||
|
|
4a5bd3135f | ||
|
|
32eb98361a | ||
|
|
2ba5073d55 | ||
|
|
9ea5e2cd90 | ||
|
|
284a1f0b57 | ||
|
|
7b97f3d535 | ||
|
|
fb431fcc7b | ||
|
|
b16100e627 | ||
|
|
fde6b5df9b | ||
|
|
dc1ac106c0 | ||
|
|
0d4a26c05e | ||
|
|
ffe7a9294b | ||
|
|
15004b6ba2 | ||
|
|
dfd8d1b0c9 | ||
|
|
c2fef3fa25 | ||
|
|
d33c892303 | ||
|
|
e4ce97cf5d | ||
|
|
f331916bd5 | ||
|
|
5d3194dd41 | ||
|
|
f2574b516e | ||
|
|
9ee3e973c1 | ||
|
|
eb9e797017 | ||
|
|
d0c305b3eb | ||
|
|
db2a7208da | ||
|
|
e8958f5e53 | ||
|
|
8acee1251f | ||
|
|
acf117e43b | ||
|
|
ed1b7f477b | ||
|
|
63cf1f5fa1 | ||
|
|
fa9b738cba | ||
|
|
3efbefe4c5 | ||
|
|
dcb797db38 | ||
|
|
8f9f9e9e82 | ||
|
|
642754a46b | ||
|
|
a5578335d3 | ||
|
|
c4309aa14c | ||
|
|
af3eb10034 | ||
|
|
88b7804123 | ||
|
|
4a383521d7 | ||
|
|
279b65cfa0 | ||
|
|
6ce644ea18 | ||
|
|
9ef5e8d037 | ||
|
|
a9e14a93dd | ||
|
|
8e2c0d8653 | ||
|
|
39a19fd9e6 | ||
|
|
68e1a0489f | ||
|
|
ece64c3f4a | ||
|
|
37f67469a6 | ||
|
|
a1b546152b | ||
|
|
3ae1f6c556 | ||
|
|
14757e2a35 | ||
|
|
d20a762dd8 | ||
|
|
5ff79f5ee1 | ||
|
|
afe28733db | ||
|
|
5e5fe6013d | ||
|
|
3f0196c8f8 | ||
|
|
4e38cf9d40 | ||
|
|
6026f6aebd | ||
|
|
856bcf1647 | ||
|
|
8e7196181c | ||
|
|
beb6697507 | ||
|
|
fd482d32a7 | ||
|
|
10169b03ce | ||
|
|
5f4a7cd7c9 | ||
|
|
59b52b8a47 | ||
|
|
02bffab38f | ||
|
|
256f08396f | ||
|
|
988a280111 | ||
|
|
7a44a457d5 | ||
|
|
2fa153e569 | ||
|
|
b589c78cfc | ||
|
|
293acbcc03 | ||
|
|
be0a841926 | ||
|
|
763cbfa656 | ||
|
|
d149b02c06 | ||
|
|
b83773dfa6 | ||
|
|
c992701387 | ||
|
|
bf43944c27 | ||
|
|
6dbed875e1 | ||
|
|
5166387f34 | ||
|
|
b44121597f | ||
|
|
f086b8abe9 | ||
|
|
795da9557b | ||
|
|
76249cb8f7 | ||
|
|
f4c4162e4b | ||
|
|
902cc9009e | ||
|
|
d93c2ee267 | ||
|
|
e5b864f07e | ||
|
|
8df4cd3dd6 | ||
|
|
2fdb6f15cb | ||
|
|
043537a7b4 | ||
|
|
e7643f3894 | ||
|
|
bb2c8ae8e5 | ||
|
|
1c8860c596 | ||
|
|
66e65fcd14 | ||
|
|
7cef8f24db | ||
|
|
a82f3a7b07 | ||
|
|
4f41068c99 | ||
|
|
50dfb95c48 | ||
|
|
0b29ac00a7 | ||
|
|
fcbe52539a | ||
|
|
bcd64286cd | ||
|
|
b89147120c | ||
|
|
c9ffd3cd11 | ||
|
|
cd62f31c17 | ||
|
|
98d63b880b | ||
|
|
04e11b0fea | ||
|
|
a873b28d9b | ||
|
|
b1b2ff6b8c | ||
|
|
cb96b5fa8f | ||
|
|
eb960209bf | ||
|
|
51a0e46f8c | ||
|
|
1251205fdd | ||
|
|
9a05629144 | ||
|
|
e0c71006d5 | ||
|
|
3d716a516a | ||
|
|
096648b2d7 | ||
|
|
02e57707de | ||
|
|
af3aa497d1 | ||
|
|
bb53d1448b | ||
|
|
eccdd73908 | ||
|
|
1d4e1a8be2 | ||
|
|
8dc0f299a9 | ||
|
|
db1ffd5091 | ||
|
|
64a27e5708 | ||
|
|
214a356135 | ||
|
|
84f7953f21 | ||
|
|
a9a947203d | ||
|
|
8f250e755e | ||
|
|
30a7fa7ead | ||
|
|
4324d655d2 | ||
|
|
da1be9226a | ||
|
|
5597bf5d1e | ||
|
|
50029fbb24 | ||
|
|
f5c2dc747d | ||
|
|
dd55e336e4 | ||
|
|
a001a45cc4 | ||
|
|
49320ff623 | ||
|
|
8e898c50b4 | ||
|
|
5fa93e2a2a | ||
|
|
178cca1611 | ||
|
|
9e543e2c5a | ||
|
|
6a16e5534d | ||
|
|
2d1cad870b | ||
|
|
3fdf255ca5 | ||
|
|
ec9225dbf2 | ||
|
|
b469fe92dd | ||
|
|
dbc54b016c | ||
|
|
2cbad36f80 | ||
|
|
8b21f33eb6 | ||
|
|
1e8ff7dbc0 | ||
|
|
0937915839 | ||
|
|
a128546954 | ||
|
|
806953d107 | ||
|
|
afa39b29ed | ||
|
|
4c899555dd | ||
|
|
3e8395b0c6 | ||
|
|
a620f02d57 | ||
|
|
79a078fb70 | ||
|
|
cac2f49b06 | ||
|
|
06993b9d66 | ||
|
|
fce5ca592a | ||
|
|
e7058cf3c8 | ||
|
|
a2eeac786e | ||
|
|
ff7873313b | ||
|
|
f3b06ac0a6 | ||
|
|
00e57fc17d | ||
|
|
c8cc59aaca | ||
|
|
54057922f6 | ||
|
|
823eea1f0a | ||
|
|
ae03a700de | ||
|
|
a089eade6e | ||
|
|
f8fb3d8a70 | ||
|
|
00a75f154e | ||
|
|
8d2313d799 | ||
|
|
0b8384fc3b | ||
|
|
73ab9efdb4 | ||
|
|
705f69510b | ||
|
|
f7e0a33935 | ||
|
|
729015d719 | ||
|
|
d83127a265 | ||
|
|
6e8f7ae698 | ||
|
|
17ee24286c | ||
|
|
133140bf71 | ||
|
|
5fafbf9ee8 | ||
|
|
6085da15a4 | ||
|
|
b93caf1839 | ||
|
|
c9787a521b | ||
|
|
26cabef74c | ||
|
|
8d2474768b | ||
|
|
a87146a401 | ||
|
|
bbf5ee5395 | ||
|
|
103ae607be | ||
|
|
6f63998000 | ||
|
|
a94952babc | ||
|
|
4b8385419e | ||
|
|
878a543818 | ||
|
|
e7337777cd | ||
|
|
fa1ed52c32 | ||
|
|
eda3fccb51 | ||
|
|
ec55fae1ec | ||
|
|
c88a98e355 | ||
|
|
0567e0f251 | ||
|
|
befa7d0e8e | ||
|
|
91913da205 | ||
|
|
cd8677a26d | ||
|
|
adf091e300 | ||
|
|
aa3b831a68 | ||
|
|
ab075c0554 | ||
|
|
4202012bbd | ||
|
|
739854935d | ||
|
|
3d88e734df | ||
|
|
a716a69b8b | ||
|
|
7ac7aacb6c | ||
|
|
7368001e3b | ||
|
|
574e0b4074 | ||
|
|
5d258eb8e1 | ||
|
|
5715fa97f7 | ||
|
|
7fe869b98b | ||
|
|
37aaa30387 | ||
|
|
c3b2d7653f | ||
|
|
cb22161156 | ||
|
|
6ad06c2d75 | ||
|
|
b291dd0ad7 | ||
|
|
3039a32f29 | ||
|
|
a8f5380070 | ||
|
|
e37867b9db | ||
|
|
110d6c81ee | ||
|
|
3da44ce604 | ||
|
|
9770c15188 | ||
|
|
cc568d9569 | ||
|
|
500a4b0b7e | ||
|
|
55cfb7b358 | ||
|
|
77b2f90259 | ||
|
|
e962baaf48 | ||
|
|
2cb5b18975 | ||
|
|
32084eb1e7 | ||
|
|
2815d76b1d | ||
|
|
9c4f146778 | ||
|
|
7b89016359 | ||
|
|
c7a4902af0 | ||
|
|
6f9b686317 | ||
|
|
16550e7a83 | ||
|
|
a9aa47e390 | ||
|
|
0846fb94db | ||
|
|
4c3aafd266 | ||
|
|
1e606f8b85 | ||
|
|
58596377b1 | ||
|
|
94a3234874 | ||
|
|
7e9db5b52d | ||
|
|
dc098d1ec7 | ||
|
|
195a8b4315 | ||
|
|
7df80bc56a | ||
|
|
1c541a4adf | ||
|
|
f29f563e50 | ||
|
|
3a94ef57e3 | ||
|
|
db8d8db280 | ||
|
|
fdcef95d07 | ||
|
|
7b1d9a777d | ||
|
|
3cd8764dbf | ||
|
|
32826440cb | ||
|
|
a65fa8cf10 | ||
|
|
0ae29b1920 | ||
|
|
5748a11788 | ||
|
|
7d3579af4f | ||
|
|
e0dc3bd1f4 | ||
|
|
6200c56144 | ||
|
|
0b4a0eeb55 | ||
|
|
467f5bd2eb | ||
|
|
322d90adfa | ||
|
|
11eb7c058f | ||
|
|
361b251952 | ||
|
|
3d503971ae | ||
|
|
9faabba361 | ||
|
|
f33629aba1 | ||
|
|
4a9b9d57e4 | ||
|
|
98c3f0ce5b | ||
|
|
78a3082bcb | ||
|
|
b64af43a7e | ||
|
|
1bcacbfebe | ||
|
|
f32db6c83b | ||
|
|
437b638973 | ||
|
|
670918efd3 |
BIN
.github/workflows/DATA
vendored
BIN
.github/workflows/DATA
vendored
Binary file not shown.
26
.github/workflows/aquamai.yml
vendored
26
.github/workflows/aquamai.yml
vendored
@@ -1,26 +0,0 @@
|
||||
name: AquaMai Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./AquaMai
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup MSBuild Path
|
||||
uses: microsoft/setup-msbuild@v1.0.2
|
||||
|
||||
- name: Decrypt DLL
|
||||
run: gpg -d --batch --passphrase "${{ secrets.DLL_PASSPHRASE }}" -o .\Libs\Assembly-CSharp.dll ..\.github\workflows\DATA
|
||||
|
||||
|
||||
- name: Build with MSBuild
|
||||
run: msbuild.exe .\AquaMai.csproj
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -75,4 +75,8 @@ gradle-app.setting
|
||||
|
||||
### Gradle Patch ###
|
||||
# Java heap dump
|
||||
*.hprof
|
||||
*.hprof
|
||||
.jpb
|
||||
src/main/resources/meta/*/*.json
|
||||
*.log.*.gz
|
||||
*.salive
|
||||
|
||||
@@ -266,8 +266,10 @@
|
||||
<Compile Include="Cheat\TicketUnlock.cs" />
|
||||
<Compile Include="Config.cs" />
|
||||
<Compile Include="Fix\FixCharaCrash.cs" />
|
||||
<Compile Include="Performance\ImproveLoadSpeed.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Main.cs" />
|
||||
<Compile Include="UX\CustomVersionString.cs" />
|
||||
<Compile Include="UX\SinglePlayer.cs" />
|
||||
<Compile Include="UX\SkipWarningScreen.cs" />
|
||||
<Compile Include="UX\SkipToMusicSelection.cs" />
|
||||
|
||||
@@ -13,4 +13,11 @@ SkipWarningScreen=true
|
||||
# Single player: Show 1P only, at the center of the screen
|
||||
SinglePlayer=true
|
||||
# !!EXPERIMENTAL!! Skip from the card-scanning screen directly to music selection screen
|
||||
SkipToMusicSelection=false
|
||||
SkipToMusicSelection=false
|
||||
# Set the version string displayed at the top-right corner of the screen
|
||||
CustomVersionString=""
|
||||
|
||||
[Performance]
|
||||
# Disable some useless checks and delays to speed up the game boot process
|
||||
# !! Known issue: The game may crash if DX Pass scanning is enabled
|
||||
ImproveLoadSpeed=false
|
||||
@@ -7,6 +7,7 @@ namespace AquaMai
|
||||
{
|
||||
public UXConfig UX { get; set; }
|
||||
public CheatConfig Cheat { get; set; }
|
||||
public PerformanceConfig Performance { get; set; }
|
||||
|
||||
public class CheatConfig
|
||||
{
|
||||
@@ -18,6 +19,12 @@ namespace AquaMai
|
||||
public bool SkipWarningScreen { get; set; }
|
||||
public bool SinglePlayer { get; set; }
|
||||
public bool SkipToMusicSelection { get; set; }
|
||||
public string CustomVersionString { get; set; }
|
||||
}
|
||||
|
||||
public class PerformanceConfig
|
||||
{
|
||||
public bool ImproveLoadSpeed { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using AquaMai.Fix;
|
||||
using AquaMai.UX;
|
||||
using MelonLoader;
|
||||
using Tomlet;
|
||||
|
||||
@@ -77,6 +78,7 @@ namespace AquaMai
|
||||
// Fixes that does not have side effects
|
||||
// These don't need to be configurable
|
||||
Patch(typeof(FixCharaCrash));
|
||||
Patch(typeof(CustomVersionString));
|
||||
|
||||
MelonLogger.Msg("Loaded!");
|
||||
}
|
||||
|
||||
63
AquaMai/Performance/ImproveLoadSpeed.cs
Normal file
63
AquaMai/Performance/ImproveLoadSpeed.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using System.Diagnostics;
|
||||
using HarmonyLib;
|
||||
using MAI2.Util;
|
||||
using Manager;
|
||||
using Process;
|
||||
|
||||
namespace AquaMai.Performance
|
||||
{
|
||||
public class ImproveLoadSpeed
|
||||
{
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(PowerOnProcess), "OnUpdate")]
|
||||
public static bool PrePowerOnUpdate(PowerOnProcess __instance)
|
||||
{
|
||||
var traverse = Traverse.Create(__instance);
|
||||
var state = traverse.Field("_state").GetValue<byte>();
|
||||
switch (state)
|
||||
{
|
||||
case 3:
|
||||
traverse.Field("_state").SetValue((byte)4);
|
||||
break;
|
||||
case 5:
|
||||
case 6:
|
||||
case 7:
|
||||
traverse.Field("_state").SetValue((byte)8);
|
||||
break;
|
||||
case 9:
|
||||
traverse.Field("_state").SetValue((byte)10);
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(StartupProcess), "OnUpdate")]
|
||||
public static bool PreStartupUpdate(StartupProcess __instance)
|
||||
{
|
||||
var traverse = Traverse.Create(__instance);
|
||||
var state = traverse.Field("_state").GetValue<byte>();
|
||||
switch (state)
|
||||
{
|
||||
case 0:
|
||||
traverse.Field("_state").SetValue((byte)1);
|
||||
break;
|
||||
case 2:
|
||||
// AimeReader maybe typeof AimeReaderManager or ChimeReaderManager, must build with correct Assembly-CSharp.dll in Libs folder
|
||||
if(SingletonStateMachine<AmManager, AmManager.EState>.Instance.AimeReader.GetType().FullName == "Manager.AimeReaderManager")
|
||||
traverse.Field("_state").SetValue((byte)3);
|
||||
break;
|
||||
case 4:
|
||||
traverse.Field("_state").SetValue((byte)5);
|
||||
break;
|
||||
case 8:
|
||||
var timer = traverse.Field("timer").GetValue<Stopwatch>();
|
||||
Traverse.Create(timer).Field("elapsed").SetValue(2 * 10000000L);
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
24
AquaMai/UX/CustomVersionString.cs
Normal file
24
AquaMai/UX/CustomVersionString.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using HarmonyLib;
|
||||
|
||||
namespace AquaMai.UX
|
||||
{
|
||||
public class CustomVersionString
|
||||
{
|
||||
/*
|
||||
* Patch displayVersionString Property Getter
|
||||
*/
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(MAI2System.Config), "displayVersionString", MethodType.Getter)]
|
||||
public static bool GetDisplayVersionString(ref string __result)
|
||||
{
|
||||
if (string.IsNullOrEmpty(AquaMai.AppConfig.UX.CustomVersionString))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
__result = AquaMai.AppConfig.UX.CustomVersionString;
|
||||
// Return false to block the original method
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
20
AquaNet/.editorconfig
Normal file
20
AquaNet/.editorconfig
Normal file
@@ -0,0 +1,20 @@
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
# Matches multiple files: .ts, .json, .svelte .sass
|
||||
[*.{json,svelte,ts,sass}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
|
||||
# Markdown files (e.g., README.md) often use a line length of 80 characters
|
||||
[*.md]
|
||||
max_line_length = 80
|
||||
trim_trailing_whitespace = false
|
||||
@@ -1,43 +0,0 @@
|
||||
# Data server for Aqua frontend
|
||||
server
|
||||
{
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
server_name aqua-data.example.com;
|
||||
|
||||
# / should redirect to the actual website aquadx.hydev.org
|
||||
location / {
|
||||
return 301 https://example.com;
|
||||
}
|
||||
|
||||
# /maimai should be a file server on /etc/nginx/aqua-data/maimai
|
||||
# These are generated using:
|
||||
|
||||
# cd Package/Sinmai_Data/StreamingAssets/A000
|
||||
# mkdir -p /etc/nginx/aqua-data/maimai
|
||||
# python3 AquaDX/tools/data_convert.py .. /etc/nginx/aqua-data/maimai/meta
|
||||
# rm -rf MovieData SoundData
|
||||
# (Open AssetRipper and open folder Package/Sinmai_Data)
|
||||
# (Export all assets to /tmp/maimai)
|
||||
# cd /tmp/maimai/ExportedProject/Assets
|
||||
# find -name "*.meta" -delete -print
|
||||
# find -name "*.asset" -delete -print
|
||||
# cp -r assetbundle Texture2D Resources/common/sprites /etc/nginx/aqua-data/maimai
|
||||
# rm -rf /tmp/maimai
|
||||
|
||||
location /maimai {
|
||||
root /etc/nginx/aqua-data;
|
||||
|
||||
# Specify UTF-8 encoding
|
||||
charset utf-8;
|
||||
|
||||
# CORS allow all
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
|
||||
|
||||
autoindex on;
|
||||
}
|
||||
|
||||
ssl_certificate /dev/null;
|
||||
ssl_certificate_key /dev/null;
|
||||
}
|
||||
@@ -15,6 +15,10 @@
|
||||
<meta name="msapplication-TileColor" content="#ffffff">
|
||||
<meta name="msapplication-config" content="/assets/icons/browserconfig.xml">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
<!-- Font CSS -->
|
||||
<link rel="stylesheet" href="/assets/fonts/Quicksand.400.css" />
|
||||
<link rel="stylesheet" href="/assets/fonts/Quicksand.500.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "aqua-net",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -12,23 +12,28 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/svelte": "^3.1.6",
|
||||
"@iconify/tools": "^4.0.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.1",
|
||||
"@tsconfig/svelte": "^5.0.2",
|
||||
"chartjs-adapter-moment": "^1.0.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-svelte": "^2.35.1",
|
||||
"sass": "^1.70.0",
|
||||
"svelte": "^4.2.10",
|
||||
"sass": "^1.71.0",
|
||||
"shiki": "^1.1.7",
|
||||
"svelte": "^4.2.11",
|
||||
"svelte-check": "^3.6.4",
|
||||
"svelte-routing": "^2.12.0",
|
||||
"svelte-turnstile": "^0.5.0",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.2.2",
|
||||
"typescript-eslint": "^7.0.1",
|
||||
"vite": "^5.1.1"
|
||||
"typescript-eslint": "^7.0.2",
|
||||
"vite": "^5.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"cal-heatmap": "^4.2.4",
|
||||
"chart.js": "^4.4.1",
|
||||
"lxgw-wenkai-lite-webfont": "^1.7.0",
|
||||
"modern-normalize": "^2.0.0",
|
||||
"moment": "^2.30.1",
|
||||
"svelte-chartjs": "^3.1.5"
|
||||
}
|
||||
|
||||
BIN
AquaNet/public/assets/email/border.png
Normal file
BIN
AquaNet/public/assets/email/border.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
27
AquaNet/public/assets/fonts/Quicksand.400.css
Normal file
27
AquaNet/public/assets/fonts/Quicksand.400.css
Normal file
@@ -0,0 +1,27 @@
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Quicksand';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(Quicksand.400.vietnamese.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Quicksand';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(Quicksand.400.latin-ext.woff2) format('woff2');
|
||||
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Quicksand';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(Quicksand.400.latin.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
BIN
AquaNet/public/assets/fonts/Quicksand.400.latin-ext.woff2
Normal file
BIN
AquaNet/public/assets/fonts/Quicksand.400.latin-ext.woff2
Normal file
Binary file not shown.
BIN
AquaNet/public/assets/fonts/Quicksand.400.latin.woff2
Normal file
BIN
AquaNet/public/assets/fonts/Quicksand.400.latin.woff2
Normal file
Binary file not shown.
BIN
AquaNet/public/assets/fonts/Quicksand.400.vietnamese.woff2
Normal file
BIN
AquaNet/public/assets/fonts/Quicksand.400.vietnamese.woff2
Normal file
Binary file not shown.
27
AquaNet/public/assets/fonts/Quicksand.500.css
Normal file
27
AquaNet/public/assets/fonts/Quicksand.500.css
Normal file
@@ -0,0 +1,27 @@
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Quicksand';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url(Quicksand.500.vietnamese.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Quicksand';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url(Quicksand.500.latin-ext.woff2) format('woff2');
|
||||
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Quicksand';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url(Quicksand.400.latin.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
BIN
AquaNet/public/assets/fonts/Quicksand.500.latin-ext.woff2
Normal file
BIN
AquaNet/public/assets/fonts/Quicksand.500.latin-ext.woff2
Normal file
Binary file not shown.
BIN
AquaNet/public/assets/fonts/Quicksand.500.latin.woff2
Normal file
BIN
AquaNet/public/assets/fonts/Quicksand.500.latin.woff2
Normal file
Binary file not shown.
BIN
AquaNet/public/assets/fonts/Quicksand.500.vietnamese.woff2
Normal file
BIN
AquaNet/public/assets/fonts/Quicksand.500.vietnamese.woff2
Normal file
Binary file not shown.
BIN
AquaNet/public/assets/imgs/no_cover.jpg
Normal file
BIN
AquaNet/public/assets/imgs/no_cover.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 894 KiB |
BIN
AquaNet/public/assets/imgs/no_cover.psd
Normal file
BIN
AquaNet/public/assets/imgs/no_cover.psd
Normal file
Binary file not shown.
BIN
AquaNet/public/assets/imgs/no_profile.png
Normal file
BIN
AquaNet/public/assets/imgs/no_profile.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 528 KiB |
BIN
AquaNet/public/assets/imgs/no_profile.psd
Normal file
BIN
AquaNet/public/assets/imgs/no_profile.psd
Normal file
Binary file not shown.
@@ -1,24 +1,62 @@
|
||||
<script lang="ts">
|
||||
import { Router, Route } from "svelte-routing";
|
||||
import Home from "./pages/Home.svelte";
|
||||
import { Route, Router } from "svelte-routing";
|
||||
import Welcome from "./pages/Welcome.svelte";
|
||||
import MaimaiRating from "./pages/MaimaiRating.svelte";
|
||||
import UserHome from "./pages/UserHome.svelte";
|
||||
import Icon from '@iconify/svelte';
|
||||
import Home from "./pages/Home.svelte";
|
||||
import Ranking from "./pages/Ranking.svelte";
|
||||
import { USER } from "./libs/sdk";
|
||||
import type { AquaNetUser } from "./libs/generalTypes";
|
||||
import Settings from "./pages/User/Settings.svelte";
|
||||
import { pfp } from "./libs/ui"
|
||||
|
||||
console.log(`%c
|
||||
┏━┓ ┳━┓━┓┏━
|
||||
┣━┫┏━┓┓ ┏┏━┓┃ ┃ ┣┫
|
||||
┛ ┗┗━┫┗━┻┗━┻┻━┛━┛┗━
|
||||
┗ v${APP_VERSION}`, `
|
||||
background: linear-gradient(-45deg, rgba(18,194,233,1) 0%, rgba(196,113,237,1) 50%, rgba(246,79,89,1) 100%);
|
||||
font-size: 2em;
|
||||
font-family: Monospace;
|
||||
unicode-bidi: isolate;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;`)
|
||||
|
||||
export let url = "";
|
||||
let me: AquaNetUser
|
||||
|
||||
if (USER.isLoggedIn()) USER.me().then(m => me = m).catch(e => console.error(e))
|
||||
|
||||
let path = window.location.pathname;
|
||||
</script>
|
||||
|
||||
<nav>
|
||||
<div>home</div>
|
||||
<div>maps</div>
|
||||
<div>rankings</div>
|
||||
<div><Icon icon="tabler:search" /></div>
|
||||
{#if path !== "/"}
|
||||
<a class="logo" href={USER.isLoggedIn() ? "/home" : "/"}>
|
||||
<img src="/assets/icons/android-chrome-192x192.png" alt="AquaDX"/>
|
||||
<span>AquaNet</span>
|
||||
</a>
|
||||
{/if}
|
||||
<a href="/home">home</a>
|
||||
<div on:click={() => alert("Coming soon!")} on:keydown={e => e.key === "Enter" && alert("Coming soon!")}
|
||||
role="button" tabindex="0">maps</div>
|
||||
<a href="/ranking">rankings</a>
|
||||
{#if me}
|
||||
<a href="/u/{me.username}">
|
||||
<img alt="profile" class="pfp" use:pfp={me}/>
|
||||
</a>
|
||||
{/if}
|
||||
</nav>
|
||||
|
||||
<Router {url}>
|
||||
<Route path="/"><Home /></Route>
|
||||
<Route path="/u/:userId" component={UserHome}></Route>
|
||||
<Route path="/u/:userId/mai/rating" component={MaimaiRating}></Route>
|
||||
<Route path="/" component={Welcome} />
|
||||
<Route path="/home" component={Home} />
|
||||
<Route path="/ranking" component={Ranking} />
|
||||
<Route path="/ranking/:game" component={Ranking} />
|
||||
<Route path="/u/:username" component={UserHome} />
|
||||
<Route path="/u/:username/:game" component={UserHome} />
|
||||
<Route path="/u/:username/:game/rating" component={MaimaiRating} />
|
||||
<Route path="/settings" component={Settings} />
|
||||
</Router>
|
||||
|
||||
<style lang="sass">
|
||||
@@ -36,21 +74,30 @@
|
||||
z-index: 10
|
||||
position: relative
|
||||
|
||||
> div
|
||||
cursor: pointer
|
||||
transition: all 0.2s ease
|
||||
text-decoration: underline 1px solid transparent
|
||||
text-underline-offset: 0.1em
|
||||
img
|
||||
width: 1.5rem
|
||||
height: 1.5rem
|
||||
border-radius: 50%
|
||||
object-fit: cover
|
||||
|
||||
.pfp
|
||||
width: 2rem
|
||||
height: 2rem
|
||||
|
||||
.logo
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: 8px
|
||||
font-weight: bold
|
||||
color: $c-main
|
||||
letter-spacing: 0.2em
|
||||
flex: 1
|
||||
|
||||
&:hover
|
||||
color: $c-main
|
||||
text-decoration-color: $c-main
|
||||
text-underline-offset: 0.5em
|
||||
@media (max-width: $w-mobile)
|
||||
> span
|
||||
display: none
|
||||
|
||||
@media (max-width: $w-mobile)
|
||||
justify-content: center
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
@import "vars"
|
||||
@import url('https://fonts.googleapis.com/css2?family=Quicksand&display=swap')
|
||||
@import url('https://fonts.googleapis.com/css2?family=Quicksand:wght@500&display=swap')
|
||||
@import 'lxgw-wenkai-lite-webfont/style.css'
|
||||
|
||||
html
|
||||
height: 100%
|
||||
@@ -25,7 +24,7 @@ body
|
||||
|
||||
a
|
||||
font-weight: 500
|
||||
color: $c-darker
|
||||
color: $c-main
|
||||
text-decoration: inherit
|
||||
|
||||
a:hover
|
||||
@@ -37,7 +36,22 @@ h1
|
||||
|
||||
|
||||
.card
|
||||
padding: 2em
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
border-radius: $border-radius
|
||||
padding: 12px 16px
|
||||
background: $ov-light
|
||||
|
||||
|
||||
blockquote
|
||||
$c1: rgba(255, 149, 149, 0.05)
|
||||
$c2: rgba(255, 152, 152, 0.12)
|
||||
background: repeating-linear-gradient(45deg, $c1, $c1 10px, $c2 10px, $c2 20px)
|
||||
padding: 10px 20px 10px 20px
|
||||
margin: 16px 0
|
||||
border-left: solid #ff7c7c 3px
|
||||
border-radius: $border-radius
|
||||
|
||||
|
||||
#app
|
||||
@@ -47,29 +61,260 @@ h1
|
||||
|
||||
main:not(.no-margin)
|
||||
max-width: 1280px
|
||||
//width: 100%
|
||||
|
||||
margin: 0 auto
|
||||
padding-bottom: 100px
|
||||
|
||||
|
||||
button
|
||||
border-radius: 8px
|
||||
border-radius: $border-radius
|
||||
border: 1px solid transparent
|
||||
padding: 0.6em 1.2em
|
||||
font-size: 1em
|
||||
font-weight: 500
|
||||
font-family: inherit
|
||||
background-color: #1a1a1a
|
||||
background-color: $ov-lighter
|
||||
opacity: 0.9
|
||||
cursor: pointer
|
||||
transition: all 0.25s
|
||||
|
||||
transition: $transition
|
||||
|
||||
button:hover
|
||||
border: 1px solid $c-main
|
||||
|
||||
border-color: $c-main
|
||||
|
||||
button:focus, button:focus-visible
|
||||
color: $c-main
|
||||
outline: none
|
||||
|
||||
button.error
|
||||
color: unset
|
||||
&:hover
|
||||
border-color: $c-error
|
||||
color: $c-error
|
||||
//background: $c-error
|
||||
//border-color: transparent
|
||||
|
||||
button.icon
|
||||
padding: 0.6em
|
||||
font-size: 1.2em
|
||||
border-radius: 50px
|
||||
@extend .flex-center
|
||||
|
||||
.level-0
|
||||
//--lv-color: #6ED43E
|
||||
--lv-color: 110, 212, 62
|
||||
|
||||
.level-1
|
||||
//--lv-color: #F7B807
|
||||
--lv-color: 247, 184, 7
|
||||
|
||||
.level-2
|
||||
//--lv-color: #FF828D
|
||||
--lv-color: 255, 130, 141
|
||||
|
||||
.level-3
|
||||
//--lv-color: #A051DC
|
||||
--lv-color: 160, 81, 220
|
||||
|
||||
.level-4
|
||||
//--lv-color: #c299e7
|
||||
--lv-color: 194, 153, 231
|
||||
|
||||
.error
|
||||
color: $c-error
|
||||
|
||||
input
|
||||
border-radius: $border-radius
|
||||
border: 1px solid transparent
|
||||
padding: 0.6em 1.2em
|
||||
font-size: 1em
|
||||
font-weight: 500
|
||||
font-family: inherit
|
||||
background-color: $ov-lighter
|
||||
transition: $transition
|
||||
box-sizing: border-box
|
||||
|
||||
input[type="checkbox"]
|
||||
width: 1.2em
|
||||
height: 1.2em
|
||||
margin: 0
|
||||
padding: 0
|
||||
border: 1px solid $c-main
|
||||
background-color: $ov-lighter
|
||||
appearance: none
|
||||
cursor: pointer
|
||||
flex-shrink: 0
|
||||
|
||||
&:checked
|
||||
background-color: $c-main
|
||||
border-color: $c-main
|
||||
|
||||
label
|
||||
cursor: pointer
|
||||
|
||||
input:focus, input:focus-visible
|
||||
border: 1px solid $c-main
|
||||
outline: none
|
||||
|
||||
input.error
|
||||
border: 1px solid $c-error
|
||||
|
||||
.flex-center
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
|
||||
|
||||
.inline-flex-center
|
||||
display: inline-flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
|
||||
.clickable
|
||||
cursor: pointer
|
||||
user-select: none
|
||||
|
||||
|
||||
// Content containers
|
||||
.content-main
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 20px
|
||||
margin: 100px auto 0
|
||||
padding: 32px 32px 128px
|
||||
min-height: 100%
|
||||
max-width: $w-max
|
||||
|
||||
background-color: darken($c-bg, 3%)
|
||||
border-radius: 16px 16px 0 0
|
||||
|
||||
@media (max-width: #{$w-max + (64px) * 2})
|
||||
margin: 100px 32px 0
|
||||
|
||||
@media (max-width: $w-mobile)
|
||||
margin: 100px 0 0
|
||||
|
||||
.fw-block
|
||||
margin-left: -32px
|
||||
margin-right: -32px
|
||||
padding: 12px 32px
|
||||
background-color: $ov-darker
|
||||
// Inner shadow
|
||||
box-shadow: inset 0 10px 10px -2px $c-shadow, inset 0 -10px 10px -2px $c-shadow
|
||||
|
||||
> h2.outer-title, > .outer-title-options
|
||||
margin-top: -5rem
|
||||
margin-bottom: 1rem
|
||||
|
||||
@media (max-width: $w-mobile)
|
||||
text-align: center
|
||||
|
||||
> .outer-title-options
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
align-items: center
|
||||
|
||||
nav
|
||||
display: flex
|
||||
flex-direction: row
|
||||
gap: 10px
|
||||
top: 4px
|
||||
|
||||
@media (max-width: $w-mobile)
|
||||
flex-direction: column
|
||||
|
||||
> h2, > .outer-title-options > h2
|
||||
margin: 0
|
||||
|
||||
main.content
|
||||
@extend .content-main
|
||||
|
||||
// Not used. still need a lot of work
|
||||
.content-popup
|
||||
position: absolute
|
||||
inset: 0
|
||||
|
||||
> div
|
||||
@extend .content-main
|
||||
position: absolute
|
||||
inset: 0
|
||||
top: 100px
|
||||
background: rgba(darken($c-bg, 3%), 0.9)
|
||||
backdrop-filter: blur(5px)
|
||||
box-shadow: 0 0 10px 6px rgba(black, 0.4)
|
||||
max-width: calc($w-max + 20px)
|
||||
|
||||
@media (max-width: #{$w-max + (64px) * 2})
|
||||
margin: 100px 22px 0
|
||||
|
||||
@media (max-width: $w-mobile)
|
||||
margin: 100px 0 0
|
||||
|
||||
|
||||
|
||||
// Overlay
|
||||
.overlay
|
||||
position: fixed
|
||||
inset: 0
|
||||
|
||||
background-color: rgba(0, 0, 0, 0.5)
|
||||
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
|
||||
z-index: 1000
|
||||
backdrop-filter: blur(5px)
|
||||
|
||||
h2, p
|
||||
user-select: none
|
||||
margin: 0
|
||||
|
||||
> div
|
||||
background-color: $c-bg
|
||||
padding: 2rem
|
||||
border-radius: $border-radius
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 1rem
|
||||
|
||||
max-width: 400px
|
||||
|
||||
.no-margin
|
||||
margin: 0
|
||||
|
||||
nav
|
||||
> div, > a
|
||||
cursor: pointer
|
||||
transition: $transition
|
||||
text-decoration: underline 1px solid transparent
|
||||
text-underline-offset: 0.1em
|
||||
|
||||
display: flex
|
||||
align-items: center
|
||||
color: unset
|
||||
font-weight: unset
|
||||
|
||||
&:hover
|
||||
color: $c-main
|
||||
text-decoration-color: $c-main
|
||||
text-underline-offset: 0.5em
|
||||
|
||||
&.active
|
||||
color: $c-main
|
||||
|
||||
|
||||
.hide-scrollbar
|
||||
&::-webkit-scrollbar
|
||||
display: none
|
||||
-ms-overflow-style: none
|
||||
scrollbar-width: none
|
||||
|
||||
|
||||
.aqua-tooltip
|
||||
box-shadow: 0 0 5px 0 $c-shadow
|
||||
border-radius: $border-radius
|
||||
position: absolute
|
||||
padding: 4px 8px
|
||||
background: $ov-lighter
|
||||
backdrop-filter: blur(5px)
|
||||
|
||||
76
AquaNet/src/components/ActionCard.svelte
Normal file
76
AquaNet/src/components/ActionCard.svelte
Normal file
@@ -0,0 +1,76 @@
|
||||
<!-- Svelte 4.2.11 -->
|
||||
|
||||
<script lang="ts">
|
||||
import Icon from "@iconify/svelte";
|
||||
|
||||
export let color: string = '179, 198, 255'
|
||||
export let icon: string
|
||||
|
||||
// Manually positioned icons
|
||||
const iconPos = [
|
||||
[1, 0.5, 2],
|
||||
[6, 2, 1.5],
|
||||
[-0.5, 4.5, 1.3],
|
||||
[5, -0.5],
|
||||
[3.5, 4.5],
|
||||
[9.5, 0.3, 1.2],
|
||||
[12.5, 2.5, 0.8],
|
||||
[10, 4.4, 0.8],
|
||||
]
|
||||
</script>
|
||||
|
||||
<div class="action-card" style="--card-color: {color}" on:click role="button" tabindex="0" on:keydown>
|
||||
<slot/>
|
||||
|
||||
<div class="icons">
|
||||
{#each iconPos as [x, y, size], i}
|
||||
<Icon icon={icon} style={`top: ${y}rem; right: ${x}rem; font-size: ${size || 1}em`} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="sass">
|
||||
@import '../vars'
|
||||
|
||||
.action-card
|
||||
overflow: hidden
|
||||
padding: 1rem
|
||||
border-radius: $border-radius
|
||||
box-shadow: 0 5px 5px 1px $c-shadow
|
||||
transition: all 0.2s ease
|
||||
cursor: pointer
|
||||
position: relative
|
||||
background: linear-gradient(45deg, transparent 20%, rgba(var(--card-color), 0.5) 100%)
|
||||
outline: 1px solid transparent
|
||||
filter: drop-shadow(0 0 12px rgba(var(--card-color), 0))
|
||||
|
||||
&:hover
|
||||
box-shadow: 0 0 0.5rem 0.2rem $c-shadow
|
||||
transform: translateY(-3px)
|
||||
|
||||
// Drop shadow glow
|
||||
filter: drop-shadow(0 0 12px rgba(var(--card-color), 0.5))
|
||||
outline-color: rgba(var(--card-color), 0.5)
|
||||
|
||||
span
|
||||
font-size: 1.2rem
|
||||
display: block
|
||||
margin-bottom: 0.5rem
|
||||
|
||||
.icons
|
||||
position: absolute
|
||||
inset: 0
|
||||
color: rgba(var(--card-color), 0.5)
|
||||
font-size: 2rem
|
||||
transition: all 0.2s ease
|
||||
z-index: -1
|
||||
mask-image: linear-gradient(45deg, transparent 30%, rgba(255,255,255,0.5) 70%, white 100%)
|
||||
opacity: 0.8
|
||||
|
||||
@media (max-width: $w-mobile)
|
||||
opacity: 0.6
|
||||
|
||||
:global(> svg)
|
||||
position: absolute
|
||||
rotate: 20deg
|
||||
</style>
|
||||
153
AquaNet/src/components/MapDetails.svelte
Normal file
153
AquaNet/src/components/MapDetails.svelte
Normal file
@@ -0,0 +1,153 @@
|
||||
<!-- Svelte 4.2.11 -->
|
||||
|
||||
<script lang="ts">
|
||||
import { slide } from "svelte/transition";
|
||||
import { DATA_HOST } from "../libs/config";
|
||||
import { t } from "../libs/i18n";
|
||||
import { type GameName, getMult } from "../libs/scoring";
|
||||
export let g: string
|
||||
export let meta: MusicMeta
|
||||
export let game: GameName
|
||||
import { coverNotFound } from "../libs/ui";
|
||||
import { DATA } from "../libs/sdk";
|
||||
import type { MusicMeta } from "../libs/generalTypes";
|
||||
import { parse } from "svelte/compiler";
|
||||
|
||||
let mapData = g.split(":").map(Number)
|
||||
let mult = getMult(mapData[3], game)
|
||||
let mapRank = parseFloat(meta.notes?.[mapData[1] === 10 ? 0 : mapData[1]]?.lv?.toFixed(1) ?? mapData[1] ?? '0')
|
||||
|
||||
</script>
|
||||
|
||||
<div class="map-detail-container" transition:slide>
|
||||
<div class="scores">
|
||||
<div>
|
||||
<img src={`${DATA_HOST}/d/mai2/music/00${mapData[0].toString().padStart(6, '0').substring(2)}.png`} alt="" on:error={coverNotFound} />
|
||||
<div class="info">
|
||||
<div class="first-line">
|
||||
<div class="song-title">{meta.name ?? t("UserHome.UnknownSong")}</div>
|
||||
<span class={`lv level-${mapData[1] === 10 ? 3 : mapData[1]}`}>
|
||||
{ mapRank }
|
||||
</span>
|
||||
</div>
|
||||
<div class="second-line">
|
||||
<span class={`rank-${getMult(mapData[3], game)[2].toString()[0]}`}>
|
||||
|
||||
<span class="rank-text">{("" + getMult(mapData[3], game)[2]).replace("p", "+")}</span>
|
||||
<span class="rank-num">{(mapData[3] / 10000).toFixed(2)}%</span>
|
||||
</span>
|
||||
{#if game === 'mai2'}
|
||||
<span class:increased={true} class="dx-change">
|
||||
{ (mapData[3] / 1000000 * mapRank * Number(mult[1])).toFixed(0) }
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="sass">
|
||||
|
||||
@import "../vars"
|
||||
$gap: 20px
|
||||
|
||||
.map-detail-container
|
||||
background-color: rgb(35,35,35)
|
||||
border-radius: $border-radius
|
||||
overflow: hidden
|
||||
|
||||
.scores
|
||||
display: flex
|
||||
flex-direction: column
|
||||
flex-wrap: wrap
|
||||
gap: $gap
|
||||
|
||||
// Image and song info
|
||||
> div
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: 12px
|
||||
max-width: 100%
|
||||
box-sizing: border-box
|
||||
|
||||
img
|
||||
width: 50px
|
||||
height: 50px
|
||||
border-radius: $border-radius
|
||||
object-fit: cover
|
||||
|
||||
// Song info and score
|
||||
> div.info
|
||||
flex: 1
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
overflow: hidden
|
||||
flex-direction: column
|
||||
|
||||
.first-line
|
||||
display: flex
|
||||
flex-direction: row
|
||||
|
||||
// Limit song name to one line
|
||||
.song-title
|
||||
flex: 1
|
||||
min-width: 0
|
||||
overflow: hidden
|
||||
text-overflow: ellipsis
|
||||
white-space: nowrap
|
||||
|
||||
// Make song score and rank not wrap
|
||||
> div:last-child
|
||||
white-space: nowrap
|
||||
|
||||
@media (max-width: $w-mobile)
|
||||
flex-direction: column
|
||||
gap: 0
|
||||
|
||||
.rank-text
|
||||
text-align: left
|
||||
|
||||
.rank-S
|
||||
// Gold green gradient on text
|
||||
background: $grad-special
|
||||
-webkit-background-clip: text
|
||||
color: transparent
|
||||
|
||||
.rank-A
|
||||
color: #ff8a8a
|
||||
|
||||
.rank-B
|
||||
color: #6ba6ff
|
||||
|
||||
.lv
|
||||
width: 30px
|
||||
text-align: center
|
||||
background: rgba(var(--lv-color), 0.6)
|
||||
padding: 0 6px
|
||||
border-radius: 0 $border-radius 0 $border-radius
|
||||
|
||||
// Inset shadow, like it's a paper below this card with a cut
|
||||
box-shadow: inset 0 0 10px rgba(0,0,0,0.5)
|
||||
|
||||
span
|
||||
display: inline-block
|
||||
text-align: left
|
||||
|
||||
.second-line
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
align-items: center
|
||||
|
||||
// Vertical table-like alignment
|
||||
span.rank-text
|
||||
min-width: 40px
|
||||
span.rank-num
|
||||
min-width: 60px
|
||||
span.dx-change
|
||||
min-width: 50px
|
||||
|
||||
span.increased
|
||||
&:before
|
||||
content: "+"
|
||||
color: $c-good
|
||||
64
AquaNet/src/components/RankDetails.svelte
Normal file
64
AquaNet/src/components/RankDetails.svelte
Normal file
@@ -0,0 +1,64 @@
|
||||
<!-- Svelte 4.2.11 -->
|
||||
|
||||
<script lang="ts">
|
||||
import { slide } from "svelte/transition";
|
||||
import { t } from "../libs/i18n";
|
||||
import type { GenericGameSummary } from "../libs/generalTypes";
|
||||
|
||||
export let g: GenericGameSummary
|
||||
|
||||
const detail = Object.entries(g.detailedRanks).toSorted((a, b) => +b[0] - +a[0])
|
||||
</script>
|
||||
|
||||
<div class="rank-detail-container fw-block" transition:slide>
|
||||
<div>
|
||||
<h2>{t("UserHome.RankDetail.Title")}</h2>
|
||||
<table>
|
||||
<!-- rankDetails: { Level : { Rank : Count } } -->
|
||||
<!-- Rows are levels, columns are ranks -->
|
||||
|
||||
<!-- Headers -->
|
||||
<tr>
|
||||
<th>{t("UserHome.RankDetail.Level")}</th>
|
||||
{#each Object.values(g.ranks) as rankMap}<th>{rankMap.name}</th>{/each}
|
||||
</tr>
|
||||
|
||||
<!-- Data -->
|
||||
{#each detail as [level, rankMap]}
|
||||
<tr>
|
||||
<td>{level}</td>
|
||||
{#each Object.values(rankMap) as count}<td>{count}</td>{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="sass">
|
||||
@import "../vars"
|
||||
|
||||
.rank-detail-container
|
||||
> div
|
||||
margin: 1em auto
|
||||
max-width: 500px
|
||||
|
||||
table
|
||||
width: 100%
|
||||
border-collapse: collapse
|
||||
table-layout: fixed
|
||||
|
||||
th:not(:first-child)
|
||||
background: $grad-special
|
||||
-webkit-background-clip: text
|
||||
-webkit-text-fill-color: transparent
|
||||
background-clip: text
|
||||
color: $c-main
|
||||
padding: 0.5em
|
||||
|
||||
th, td
|
||||
padding: 0.5em
|
||||
text-align: center
|
||||
|
||||
&:first-child
|
||||
color: $c-main
|
||||
</style>
|
||||
87
AquaNet/src/components/StatusOverlays.svelte
Normal file
87
AquaNet/src/components/StatusOverlays.svelte
Normal file
@@ -0,0 +1,87 @@
|
||||
<!-- Svelte 4.2.11 -->
|
||||
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition'
|
||||
import type { ConfirmProps } from "../libs/generalTypes";
|
||||
import { DISCORD_INVITE } from "../libs/config";
|
||||
import Icon from "@iconify/svelte";
|
||||
import { t } from "../libs/i18n"
|
||||
|
||||
// Props
|
||||
export let confirm: ConfirmProps | null = null
|
||||
export let error: string | null
|
||||
export let loading: boolean = false
|
||||
</script>
|
||||
|
||||
{#if confirm}
|
||||
<div class="overlay" transition:fade>
|
||||
<div>
|
||||
<h2>{confirm.title}</h2>
|
||||
<span>{confirm.message}</span>
|
||||
|
||||
<div class="actions">
|
||||
{#if confirm.cancel}
|
||||
<!-- Svelte LSP is very annoying here -->
|
||||
<button on:click={() => {
|
||||
confirm && confirm.cancel && confirm.cancel()
|
||||
|
||||
// Set to null
|
||||
confirm = null
|
||||
}}>{t('action.cancel')}</button>
|
||||
{/if}
|
||||
<button on:click={() => confirm && confirm.confirm()} class:error={confirm.dangerous}>{t('action.confirm')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if error}
|
||||
<div class="overlay" transition:fade>
|
||||
<div>
|
||||
<h2 class="error">{t('status.error')}</h2>
|
||||
<span>{t('status.error.hint')}<a href={DISCORD_INVITE}>{t('status.error.hint.link')}</a></span>
|
||||
<span>{t('status.detail', { detail: error })}</span>
|
||||
|
||||
<div class="actions">
|
||||
<button on:click={() => location.reload()} class="error">
|
||||
{t('action.refresh')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if loading && !error}
|
||||
<div class="overlay loading" transition:fade>
|
||||
<Icon class="icon" icon="svg-spinners:pulse-2"/>
|
||||
<span><span>LOADING</span></span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="sass">
|
||||
.actions
|
||||
display: flex
|
||||
gap: 16px
|
||||
|
||||
button
|
||||
width: 100%
|
||||
|
||||
.loading.overlay
|
||||
font-size: 28rem
|
||||
|
||||
:global(.icon)
|
||||
opacity: 0.5
|
||||
|
||||
> span
|
||||
position: absolute
|
||||
inset: 0
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
background: transparent
|
||||
|
||||
letter-spacing: 20px
|
||||
margin-left: 20px
|
||||
|
||||
font-size: 1.5rem
|
||||
</style>
|
||||
@@ -1,5 +1,15 @@
|
||||
|
||||
const aqua_host = 'https://aquanet.example.com/aqua'
|
||||
const data_host = 'https://aquanet.example.com'
|
||||
|
||||
export { aqua_host, data_host }
|
||||
|
||||
export const AQUA_HOST = 'https://aquadx.net/aqua'
|
||||
export const DATA_HOST = 'https://aquadx.net'
|
||||
|
||||
// This will be displayed for users to connect from the client
|
||||
export const AQUA_CONNECTION = 'aquadx.hydev.org'
|
||||
|
||||
export const TURNSTILE_SITE_KEY = '0x4AAAAAAASGA2KQEIelo9P9'
|
||||
export const DISCORD_INVITE = 'https://discord.gg/FNgveqFF7s'
|
||||
|
||||
// UI
|
||||
export const FADE_OUT = { duration: 200 }
|
||||
export const FADE_IN = { delay: 400 }
|
||||
export const DEFAULT_PFP = "/assets/imgs/no_profile.png"
|
||||
|
||||
|
||||
@@ -2,4 +2,121 @@ export interface TrendEntry {
|
||||
date: string
|
||||
rating: number
|
||||
plays: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface Card {
|
||||
luid: string
|
||||
registerTime: string
|
||||
accessTime: string
|
||||
linked: boolean
|
||||
ghost: boolean
|
||||
}
|
||||
|
||||
export interface AquaNetUser {
|
||||
username: string
|
||||
email: string
|
||||
displayName: string
|
||||
country: string
|
||||
lastLogin: number
|
||||
regTime: number
|
||||
profileLocation: string
|
||||
profileBio: string
|
||||
profilePicture: string
|
||||
emailConfirmed: boolean
|
||||
ghostCard: Card
|
||||
cards: Card[]
|
||||
computedName: string,
|
||||
}
|
||||
|
||||
export interface CardSummaryGame {
|
||||
name: string
|
||||
rating: number
|
||||
lastLogin: string
|
||||
}
|
||||
|
||||
export interface CardSummary {
|
||||
mai2: CardSummaryGame | null
|
||||
chu3: CardSummaryGame | null
|
||||
ongeki: CardSummaryGame | null
|
||||
diva: CardSummaryGame | null
|
||||
}
|
||||
|
||||
|
||||
export interface ConfirmProps {
|
||||
title: string
|
||||
message: string
|
||||
confirm: () => void
|
||||
cancel?: () => void
|
||||
dangerous?: boolean
|
||||
}
|
||||
|
||||
export interface GenericGamePlaylog {
|
||||
musicId: number
|
||||
level: number
|
||||
playDate: string
|
||||
achievement: number
|
||||
maxCombo: number
|
||||
totalCombo: number
|
||||
afterRating: number
|
||||
beforeRating: number
|
||||
}
|
||||
|
||||
export interface GenericRanking {
|
||||
name: string
|
||||
username: string
|
||||
rank: number
|
||||
accuracy: number
|
||||
rating: number
|
||||
fullCombo: number
|
||||
allPerfect: number
|
||||
lastSeen: string
|
||||
}
|
||||
|
||||
export interface RankCount {
|
||||
name: string
|
||||
count: number
|
||||
}
|
||||
|
||||
export interface GenericGameSummary {
|
||||
name: string
|
||||
iconId: number
|
||||
aquaUser?: AquaNetUser
|
||||
serverRank: number
|
||||
accuracy: number
|
||||
rating: number
|
||||
ratingHighest: number
|
||||
ranks: RankCount[]
|
||||
detailedRanks: { [key: number]: { [key: string]: number } }
|
||||
maxCombo: number
|
||||
fullCombo: number
|
||||
allPerfect: number
|
||||
totalScore: number
|
||||
plays: number
|
||||
totalPlayTime: number
|
||||
joined: string
|
||||
lastSeen: string
|
||||
lastVersion: string
|
||||
ratingComposition: { [key: string]: any }
|
||||
recent: GenericGamePlaylog[]
|
||||
}
|
||||
|
||||
export interface MusicMeta {
|
||||
name: string,
|
||||
composer: string,
|
||||
bpm: number,
|
||||
ver: number,
|
||||
notes: {
|
||||
lv: number
|
||||
designer: string
|
||||
lv_id: number
|
||||
notes: number
|
||||
}[]
|
||||
}
|
||||
|
||||
export type AllMusic = { [key: string]: MusicMeta }
|
||||
|
||||
export interface GameOption {
|
||||
key: string
|
||||
value: any
|
||||
type: "Boolean"
|
||||
}
|
||||
|
||||
52
AquaNet/src/libs/i18n.ts
Normal file
52
AquaNet/src/libs/i18n.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { EN_REF, type LocalizedMessages } from "./i18n/en_ref";
|
||||
import { ZH } from "./i18n/zh";
|
||||
import type { GameName } from "./scoring";
|
||||
|
||||
type Lang = 'en' | 'zh'
|
||||
|
||||
const msgs: Record<Lang, LocalizedMessages> = {
|
||||
en: EN_REF,
|
||||
zh: ZH
|
||||
}
|
||||
|
||||
|
||||
let lang: Lang = 'en'
|
||||
|
||||
// Infer language from browser
|
||||
if (navigator.language.startsWith('zh')) {
|
||||
lang = 'zh'
|
||||
}
|
||||
|
||||
export function ts(key: string, variables?: { [index: string]: any }) {
|
||||
return t(key as keyof LocalizedMessages, variables)
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the translation for the given key
|
||||
*
|
||||
* TODO: Check for translation completion on build
|
||||
*
|
||||
* @param key
|
||||
* @param variables
|
||||
*/
|
||||
export function t(key: keyof LocalizedMessages, variables?: { [index: string]: any }) {
|
||||
// Check if the key exists
|
||||
let msg = msgs[lang][key]
|
||||
if (!msg) {
|
||||
// Check if the key exists in English
|
||||
if (!(msg = msgs.en[key])) {
|
||||
msg = key
|
||||
console.error(`ERROR!! Missing translation reference entry (English) for ${key}`)
|
||||
}
|
||||
else console.warn(`Missing translation for ${key} in ${lang}`)
|
||||
}
|
||||
// Replace variables
|
||||
if (variables) {
|
||||
return msg.replace(/\${(.*?)}/g, (_: string, v: string | number) => variables[v] + "")
|
||||
}
|
||||
return msg
|
||||
}
|
||||
Object.assign(window, { t })
|
||||
|
||||
export const GAME_TITLE: { [key in GameName]: string } =
|
||||
{chu3: t("game.chu3"), mai2: t("game.mai2"), ongeki: t("game.ongeki"), wacca: t("game.wacca")}
|
||||
109
AquaNet/src/libs/i18n/en_ref.ts
Normal file
109
AquaNet/src/libs/i18n/en_ref.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
export const EN_REF_USER = {
|
||||
'UserHome.ServerRank': 'Server Rank',
|
||||
'UserHome.DXRating': 'DX Rating',
|
||||
'UserHome.Rating': 'Rating',
|
||||
'UserHome.Statistics': 'Statistics',
|
||||
'UserHome.Accuracy': 'Accuracy',
|
||||
'UserHome.MaxCombo': 'Max Combo',
|
||||
'UserHome.FullCombo': 'Full Combo',
|
||||
'UserHome.AllPerfect': 'All Perfect',
|
||||
'UserHome.DXScore': 'DX Score',
|
||||
'UserHome.Score': 'Score',
|
||||
'UserHome.PlayActivity': ' Play Activity',
|
||||
'UserHome.Plays': 'Plays',
|
||||
'UserHome.PlayTime': 'Play Time',
|
||||
'UserHome.FirstSeen': 'First Seen',
|
||||
'UserHome.LastSeen': 'Last Seen',
|
||||
'UserHome.Version': 'Last Version',
|
||||
'UserHome.RecentScores': 'Recent Scores',
|
||||
'UserHome.NoData': 'No data in the past ${days} days',
|
||||
'UserHome.UnknownSong': "(unknown song)",
|
||||
'UserHome.Settings': 'Settings',
|
||||
'UserHome.NoValidGame': "The user hasn't played any game yet.",
|
||||
'UserHome.ShowRanksDetails': "Click to show details",
|
||||
'UserHome.RankDetail.Title': 'Achievement Details',
|
||||
'UserHome.RankDetail.Level': "Level",
|
||||
'UserHome.B50': "B50",
|
||||
}
|
||||
|
||||
export const EN_REF_Welcome = {
|
||||
'back': 'Back',
|
||||
'email': 'Email',
|
||||
'password': 'Password',
|
||||
'username': 'Username',
|
||||
'welcome.btn-login': 'Log in',
|
||||
'welcome.btn-signup': 'Sign up',
|
||||
'welcome.email-password-missing': 'Email and password are required',
|
||||
'welcome.username-missing': 'Username/email is required',
|
||||
'welcome.waiting-turnstile': 'Waiting for Turnstile to verify your network environment...',
|
||||
'welcome.turnstile-error': 'Error verifying your network environment. Please turn off your VPN and try again.',
|
||||
'welcome.turnstile-timeout': 'Network verification timed out. Please try again.',
|
||||
'welcome.verification-sent': 'A verification email has been sent to ${email}. Please check your inbox!',
|
||||
'welcome.verify-state-0': 'You haven\'t verified your email. A verification email had been sent to your inbox less than a minute ago. Please check your inbox!',
|
||||
'welcome.verify-state-1': 'You haven\'t verified your email. We\'ve already sent 3 emails over the last 24 hours so we\'ll not send another one. Please check your inbox!',
|
||||
'welcome.verify-state-2': 'You haven\'t verified your email. We just sent you another verification email. Please check your inbox!',
|
||||
'welcome.verifying': 'Verifying your email... please wait.',
|
||||
'welcome.verified': 'Your email has been verified! You can now log in now.',
|
||||
'welcome.verification-failed': 'Verification failed: ${message}. Please try again.',
|
||||
}
|
||||
|
||||
export const EN_REF_LEADERBOARD = {
|
||||
'Leaderboard.Title': 'Server Leaderboard',
|
||||
'Leaderboard.Rank': 'Rank',
|
||||
'Leaderboard.Rating': 'Rating',
|
||||
'Leaderboard.Accuracy': 'Accuracy',
|
||||
'Leaderboard.FC': 'FC',
|
||||
'Leaderboard.AP': 'AP',
|
||||
}
|
||||
|
||||
export const EN_REF_GENERAL = {
|
||||
'game.mai2': "Mai",
|
||||
'game.chu3': "Chuni",
|
||||
'game.ongeki': "Ongeki",
|
||||
'game.wacca': "Wacca",
|
||||
'status.error': "Error",
|
||||
'status.error.hint': 'Something went wrong, please try again later or ',
|
||||
'status.error.hint.link': 'join our discord for support.',
|
||||
'status.detail': 'Detail: ${detail}',
|
||||
'action.refresh': 'Refresh',
|
||||
'action.cancel': 'Cancel',
|
||||
'action.confirm': 'Confirm',
|
||||
}
|
||||
|
||||
export const EN_REF_HOME = {
|
||||
'home.nav.portal': 'Portal',
|
||||
'home.nav.link-card': 'Link Card',
|
||||
'home.nav.game-setup': 'Game Setup',
|
||||
'home.manage-cards': 'Manage Cards',
|
||||
'home.manage-cards-description': 'Link, unlink, and manage your cards.',
|
||||
'home.link-card': 'Link Card',
|
||||
'home.link-cards-description': 'Link your Amusement IC / Aime card to play games.',
|
||||
'home.join-discord': 'Join Discord',
|
||||
'home.join-discord-description': 'Join our Discord server to chat with other players and get help.',
|
||||
'home.setup': 'Setup Connection',
|
||||
'home.setup-description': 'If you own a cab or arcade setup, begin setting up the connection.',
|
||||
}
|
||||
|
||||
export const EN_REF_SETTINGS = {
|
||||
'settings.title': 'Settings',
|
||||
'settings.tabs.profile': 'Profile',
|
||||
'settings.tabs.game': 'Game',
|
||||
'settings.fields.unlockMusic.name': 'Unlock All Music',
|
||||
'settings.fields.unlockMusic.desc': 'Unlock all music and master difficulty in game.',
|
||||
'settings.fields.unlockChara.name': 'Unlock All Characters',
|
||||
'settings.fields.unlockChara.desc': 'Unlock all characters, voices, and partners in game.',
|
||||
'settings.fields.unlockCollectables.name': 'Unlock All Collectables',
|
||||
'settings.fields.unlockCollectables.desc': 'Unlock all collectables (nameplate, title, icon, frame) in game. ' +
|
||||
'This setting is not relevant in chusan because in-game user box is disabled.',
|
||||
'settings.fields.unlockTickets.name': 'Unlock All Tickets',
|
||||
'settings.fields.unlockTickets.desc': 'Infinite map/ex tickets (note: maimai still limits which tickets can be used).',
|
||||
'settings.fields.waccaInfiniteWp.name': 'Wacca: Infinite WP',
|
||||
'settings.fields.waccaInfiniteWp.desc': 'Set WP to 999999',
|
||||
'settings.fields.waccaAlwaysVip.name': 'Wacca: Always VIP',
|
||||
'settings.fields.waccaAlwaysVip.desc': 'Set VIP expiration date to 2077-01-01',
|
||||
}
|
||||
|
||||
export const EN_REF = { ...EN_REF_USER, ...EN_REF_Welcome, ...EN_REF_GENERAL,
|
||||
...EN_REF_LEADERBOARD, ...EN_REF_HOME, ...EN_REF_SETTINGS }
|
||||
|
||||
export type LocalizedMessages = typeof EN_REF
|
||||
115
AquaNet/src/libs/i18n/zh.ts
Normal file
115
AquaNet/src/libs/i18n/zh.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import {
|
||||
EN_REF_GENERAL,
|
||||
EN_REF_HOME,
|
||||
EN_REF_LEADERBOARD,
|
||||
EN_REF_SETTINGS,
|
||||
EN_REF_USER,
|
||||
type EN_REF_Welcome
|
||||
} from "./en_ref";
|
||||
|
||||
const zhUser: typeof EN_REF_USER = {
|
||||
'UserHome.ServerRank': '服务器排名',
|
||||
'UserHome.DXRating': 'DX B50',
|
||||
'UserHome.Rating': '评分',
|
||||
'UserHome.Statistics': '统计数据',
|
||||
'UserHome.Accuracy': '准确率',
|
||||
'UserHome.MaxCombo': '最大连击',
|
||||
'UserHome.FullCombo': '全连曲目',
|
||||
'UserHome.AllPerfect': '完美曲目',
|
||||
'UserHome.DXScore': 'DX 总分',
|
||||
'UserHome.Score': '总分',
|
||||
'UserHome.PlayActivity': '游戏活动',
|
||||
'UserHome.Plays': '出勤次数',
|
||||
'UserHome.PlayTime': '游玩时间',
|
||||
'UserHome.FirstSeen': '发现新大陆',
|
||||
'UserHome.LastSeen': '上次出勤',
|
||||
'UserHome.Version': '游戏版本',
|
||||
'UserHome.RecentScores': '成绩',
|
||||
'UserHome.NoData': '过去 ${days} 天内没有玩过',
|
||||
'UserHome.UnknownSong': "(未知曲目)",
|
||||
'UserHome.Settings': '设置',
|
||||
'UserHome.NoValidGame': "用户还没有玩过游戏",
|
||||
'UserHome.ShowRanksDetails': "点击显示评分详细",
|
||||
'UserHome.RankDetail.Title': '评分详细',
|
||||
'UserHome.RankDetail.Level': "等级",
|
||||
'UserHome.B50': "B50",
|
||||
}
|
||||
|
||||
const zhWelcome: typeof EN_REF_Welcome = {
|
||||
'back': '返回',
|
||||
'email': '邮箱',
|
||||
'password': '密码',
|
||||
'username': '用户名',
|
||||
'welcome.btn-login': '登录',
|
||||
'welcome.btn-signup': '注册',
|
||||
'welcome.email-password-missing': '邮箱和密码必须填哦',
|
||||
'welcome.username-missing': '用户名/邮箱必须填哦',
|
||||
'welcome.waiting-turnstile': '正在验证网络环境...',
|
||||
'welcome.turnstile-error': '验证网络环境出错了,请关闭VPN后重试',
|
||||
'welcome.turnstile-timeout': '验证网络环境超时了,请重试',
|
||||
'welcome.verification-sent': '验证邮件已发送至 ${email},请翻翻收件箱',
|
||||
'welcome.verify-state-0': '您还没有验证邮箱哦!验证邮件一分钟内刚刚发到您的邮箱,请翻翻收件箱',
|
||||
'welcome.verify-state-1': '您还没有验证邮箱哦!我们在过去的24小时内已经发送了3封验证邮件,所以我们不会再发送了,请翻翻收件箱',
|
||||
'welcome.verify-state-2': '您还没有验证邮箱哦!我们刚刚又发送了一封验证邮件,请翻翻收件箱',
|
||||
'welcome.verifying': '正在验证邮箱...请稍等',
|
||||
'welcome.verified': '您的邮箱已经验证成功!您现在可以登录了',
|
||||
'welcome.verification-failed': '验证失败:${message}。请重试',
|
||||
}
|
||||
|
||||
const zhLeaderboard: typeof EN_REF_LEADERBOARD = {
|
||||
'Leaderboard.Title': '排行榜',
|
||||
'Leaderboard.Rank': '排名',
|
||||
'Leaderboard.Rating': '评分',
|
||||
'Leaderboard.Accuracy': '准确率',
|
||||
'Leaderboard.FC': 'FC',
|
||||
'Leaderboard.AP': 'AP',
|
||||
}
|
||||
|
||||
const zhGeneral: typeof EN_REF_GENERAL = {
|
||||
'game.mai2': "舞萌",
|
||||
'game.chu3': "中二",
|
||||
'game.ongeki': "音击",
|
||||
'game.wacca': "Wacca",
|
||||
"status.error": "发生错误",
|
||||
"status.error.hint": "出了一些问题,请稍后刷新重试或者",
|
||||
"status.error.hint.link": "加我们的 Discord 群问一问",
|
||||
"status.detail": "详细信息:${detail}",
|
||||
"action.refresh": "刷新",
|
||||
"action.cancel": "取消",
|
||||
"action.confirm": "确认",
|
||||
}
|
||||
|
||||
const zhHome: typeof EN_REF_HOME = {
|
||||
'home.nav.portal': "主页",
|
||||
'home.nav.link-card': "绑卡",
|
||||
'home.nav.game-setup': "连接设置",
|
||||
'home.manage-cards': '管理游戏卡',
|
||||
'home.manage-cards-description': '绑定、解绑、管理游戏数据卡',
|
||||
'home.link-card': '绑定游戏卡',
|
||||
'home.link-cards-description':'绑定游戏数据卡 (Amusement IC 或 Aime 卡) 后才可以访问游戏存档哦',
|
||||
'home.join-discord': '加入 Discord',
|
||||
'home.join-discord-description': '加入我们的 Discord 群,与其他玩家聊天、获取帮助',
|
||||
'home.setup': '连接私服',
|
||||
'home.setup-description': '如果您有街机框体或者手台,点击这里设置服务器的连接',
|
||||
}
|
||||
|
||||
const zhSettings: typeof EN_REF_SETTINGS = {
|
||||
'settings.title': '用户设置',
|
||||
'settings.tabs.profile': '个人资料',
|
||||
'settings.tabs.game': '游戏设置',
|
||||
'settings.fields.unlockMusic.name': '解锁谱面',
|
||||
'settings.fields.unlockMusic.desc': '在游戏中解锁所有曲目和大师难度谱面。',
|
||||
'settings.fields.unlockChara.name': '解锁角色',
|
||||
'settings.fields.unlockChara.desc': '在游戏中解锁所有角色、语音和伙伴。',
|
||||
'settings.fields.unlockCollectables.name': '解锁收藏品',
|
||||
'settings.fields.unlockCollectables.desc': '在游戏中解锁所有收藏品(名牌、称号、图标、背景图),此设置对中二不适用。',
|
||||
'settings.fields.unlockTickets.name': '解锁游戏券',
|
||||
'settings.fields.unlockTickets.desc': '无限跑图券/解锁券(注:maimai 客户端仍限制一些券不能使用)。',
|
||||
'settings.fields.waccaInfiniteWp.name': 'Wacca: 无限 WP',
|
||||
'settings.fields.waccaInfiniteWp.desc': '将 WP 设置为 999999',
|
||||
'settings.fields.waccaAlwaysVip.name': 'Wacca: 永久会员',
|
||||
'settings.fields.waccaAlwaysVip.desc': '将 VIP 到期时间设置为 2077-01-01',
|
||||
}
|
||||
|
||||
export const ZH = { ...zhUser, ...zhWelcome, ...zhGeneral,
|
||||
...zhLeaderboard, ...zhHome, ...zhSettings }
|
||||
@@ -1,51 +1,13 @@
|
||||
import { aqua_host, data_host } from './config'
|
||||
import type { TrendEntry } from './generalTypes'
|
||||
import type { MaimaiUserSummaryEntry } from './maimaiTypes'
|
||||
|
||||
|
||||
const multTable = [
|
||||
[ 100.5, 22.4, 'SSSp' ],
|
||||
[ 100, 21.6, 'SSS' ],
|
||||
[ 99.5, 21.1, 'SSp' ],
|
||||
[ 99, 20.8, 'SS' ],
|
||||
[ 98, 20.3, 'Sp' ],
|
||||
[ 97, 20, 'S' ],
|
||||
[ 94, 16.8, 'AAA' ],
|
||||
[ 90, 15.2, 'AA' ],
|
||||
[ 80, 13.6, 'A' ]
|
||||
]
|
||||
|
||||
|
||||
export function getMult(achievement: number) {
|
||||
achievement /= 10000
|
||||
for (let i = 0; i < multTable.length; i++) {
|
||||
if (achievement >= (multTable[i][0] as number)) return multTable[i]
|
||||
}
|
||||
return [ 0, 0, 0 ]
|
||||
}
|
||||
import { AQUA_HOST, DATA_HOST } from './config'
|
||||
|
||||
|
||||
export async function getMaimai(endpoint: string, params: any) {
|
||||
return await fetch(`${aqua_host}/Maimai2Servlet/${endpoint}`, {
|
||||
return await fetch(`${AQUA_HOST}/Maimai2Servlet/${endpoint}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(params)
|
||||
}).then(res => res.json())
|
||||
}
|
||||
|
||||
export async function getMaimaiAllMusic(): Promise<{ [key: string]: any }> {
|
||||
return fetch(`${data_host}/maimai/meta/00/all-music.json`).then(it => it.json())
|
||||
return fetch(`${DATA_HOST}/maimai/meta/00/all-music.json`).then(it => it.json())
|
||||
}
|
||||
|
||||
export async function getMaimaiApi(endpoint: string, params: any) {
|
||||
const url = new URL(`${aqua_host}/api/game/maimai2new/${endpoint}`)
|
||||
Object.keys(params).forEach(key => url.searchParams.append(key, params[key]))
|
||||
return await fetch(url).then(res => res.json())
|
||||
}
|
||||
|
||||
export async function getMaimaiTrend(userId: number): Promise<TrendEntry[]> {
|
||||
return await getMaimaiApi('trend', { userId })
|
||||
}
|
||||
|
||||
export async function getMaimaiUser(userId: number): Promise<MaimaiUserSummaryEntry> {
|
||||
return await getMaimaiApi('user-summary', { userId })
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { MusicMeta } from "./generalTypes";
|
||||
|
||||
export interface Rating {
|
||||
musicId: number
|
||||
level: number
|
||||
@@ -5,24 +7,11 @@ export interface Rating {
|
||||
}
|
||||
|
||||
export interface ParsedRating extends Rating {
|
||||
music: MaimaiMusic,
|
||||
music: MusicMeta,
|
||||
calc: number,
|
||||
rank: string
|
||||
}
|
||||
|
||||
export interface MaimaiMusic {
|
||||
name: string,
|
||||
composer: string,
|
||||
bpm: number,
|
||||
ver: number,
|
||||
note: {
|
||||
lv: number
|
||||
designer: string
|
||||
lv_id: number
|
||||
notes: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface MaimaiUserSummaryEntry {
|
||||
name: string
|
||||
iconId: number
|
||||
|
||||
68
AquaNet/src/libs/scoring.ts
Normal file
68
AquaNet/src/libs/scoring.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
export type GameName = 'mai2' | 'chu3' | 'ongeki' | 'wacca'
|
||||
|
||||
const multTable = {
|
||||
'mai2': [
|
||||
[ 100.5, 22.4, 'SSSp' ],
|
||||
[ 100.0, 21.6, 'SSS' ],
|
||||
[ 99.5, 21.1, 'SSp' ],
|
||||
[ 99, 20.8, 'SS' ],
|
||||
[ 98, 20.3, 'Sp' ],
|
||||
[ 97, 20, 'S' ],
|
||||
[ 94, 16.8, 'AAA' ],
|
||||
[ 90, 15.2, 'AA' ],
|
||||
[ 80, 13.6, 'A' ]
|
||||
],
|
||||
|
||||
// TODO: Fill in multipliers for Chunithm and Ongeki
|
||||
'chu3': [
|
||||
[ 100.75, 0, 'SSS' ],
|
||||
[ 100.0, 0, 'SS' ],
|
||||
[ 97.5, 0, 'S' ],
|
||||
[ 95.0, 0, 'AAA' ],
|
||||
[ 92.5, 0, 'AA' ],
|
||||
[ 90.0, 0, 'A' ],
|
||||
[ 80.0, 0, 'BBB' ],
|
||||
[ 70.0, 0, 'BB' ],
|
||||
[ 60.0, 0, 'B' ],
|
||||
[ 50.0, 0, 'C' ],
|
||||
[ 0.0, 0, 'D' ]
|
||||
],
|
||||
|
||||
'ongeki': [
|
||||
[ 100.75, 0, 'SSS+' ],
|
||||
[ 100.0, 0, 'SSS' ],
|
||||
[ 99.0, 0, 'SS' ],
|
||||
[ 97.0, 0, 'S' ],
|
||||
[ 94.0, 0, 'AAA' ],
|
||||
[ 90.0, 0, 'AA' ],
|
||||
[ 85.0, 0, 'A' ],
|
||||
[ 80.0, 0, 'BBB' ],
|
||||
[ 75.0, 0, 'BB' ],
|
||||
[ 70.0, 0, 'B' ],
|
||||
[ 50.0, 0, 'C' ],
|
||||
[ 0.0, 0, 'D' ]
|
||||
],
|
||||
|
||||
'wacca': [
|
||||
[ 100.0, 0, 'AP' ],
|
||||
[ 98.0, 0, 'SSS' ],
|
||||
[ 95.0, 0, 'SS' ],
|
||||
[ 90.0, 0, 'S' ],
|
||||
[ 85.0, 0, 'AAA' ],
|
||||
[ 80.0, 0, 'AA' ],
|
||||
[ 70.0, 0, 'A' ],
|
||||
[ 60.0, 0, 'B' ],
|
||||
[ 1.0, 0, 'C' ],
|
||||
[ 0.0, 0, 'D' ]
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
export function getMult(achievement: number, game: GameName) {
|
||||
achievement /= 10000
|
||||
const mt = multTable[game]
|
||||
for (let i = 0; i < mt.length; i++) {
|
||||
if (achievement >= (mt[i][0] as number)) return mt[i]
|
||||
}
|
||||
return [ 0, 0, 0 ]
|
||||
}
|
||||
162
AquaNet/src/libs/sdk.ts
Normal file
162
AquaNet/src/libs/sdk.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import { AQUA_HOST, DATA_HOST } from "./config";
|
||||
import type {
|
||||
AllMusic,
|
||||
Card,
|
||||
CardSummary,
|
||||
GenericGameSummary,
|
||||
GenericRanking,
|
||||
TrendEntry,
|
||||
AquaNetUser, GameOption
|
||||
} from "./generalTypes";
|
||||
import type { GameName } from "./scoring";
|
||||
|
||||
interface RequestInitWithParams extends RequestInit {
|
||||
params?: { [index: string]: string }
|
||||
localCache?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify a fetch url
|
||||
*
|
||||
* @param input Fetch url input
|
||||
* @param callback Callback for modification
|
||||
*/
|
||||
export function reconstructUrl(input: URL | RequestInfo, callback: (url: URL) => URL | void): RequestInfo | URL {
|
||||
let u = new URL((input instanceof Request) ? input.url : input);
|
||||
const result = callback(u)
|
||||
if (result) u = result
|
||||
if (input instanceof Request) {
|
||||
// @ts-ignore
|
||||
return { url: u, ...input }
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch with url parameters
|
||||
*/
|
||||
export function fetchWithParams(input: URL | RequestInfo, init?: RequestInitWithParams): Promise<Response> {
|
||||
return fetch(reconstructUrl(input, u => {
|
||||
u.search = new URLSearchParams(init?.params ?? {}).toString()
|
||||
}), init)
|
||||
}
|
||||
|
||||
let cache: { [index: string]: any } = {}
|
||||
|
||||
export async function post(endpoint: string, params: any, init?: RequestInitWithParams): Promise<any> {
|
||||
// Add token if exists
|
||||
const token = localStorage.getItem('token')
|
||||
if (token && !('token' in params)) params = { ...(params ?? {}), token }
|
||||
|
||||
if (init?.localCache) {
|
||||
const cached = cache[endpoint + JSON.stringify(params) + JSON.stringify(init)]
|
||||
if (cached) return cached
|
||||
}
|
||||
|
||||
let res = await fetchWithParams(AQUA_HOST + endpoint, {
|
||||
method: 'POST',
|
||||
params,
|
||||
...init
|
||||
}).catch(e => {
|
||||
console.error(e)
|
||||
throw new Error('Network error')
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text()
|
||||
console.error(`${res.status}: ${text}`)
|
||||
|
||||
// If 400 invalid token is caught, should invalidate the token and redirect to signin
|
||||
if (text === 'Invalid token') {
|
||||
localStorage.removeItem('token')
|
||||
window.location.href = '/'
|
||||
}
|
||||
|
||||
// Try to parse as json
|
||||
let json
|
||||
try {
|
||||
json = JSON.parse(text)
|
||||
} catch (e) {
|
||||
throw new Error(text)
|
||||
}
|
||||
if (json.error) throw new Error(json.error)
|
||||
}
|
||||
|
||||
const ret = res.json()
|
||||
cache[endpoint + JSON.stringify(params) + JSON.stringify(init)] = ret
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
/**
|
||||
* aqua.net.UserRegistrar
|
||||
*
|
||||
* @param user
|
||||
*/
|
||||
async function register(user: { username: string, email: string, password: string, turnstile: string }) {
|
||||
return await post('/api/v2/user/register', user)
|
||||
}
|
||||
async function login(user: { email: string, password: string, turnstile: string }) {
|
||||
const data = await post('/api/v2/user/login', user)
|
||||
|
||||
// Put token into local storage
|
||||
localStorage.setItem('token', data.token)
|
||||
}
|
||||
|
||||
const isLoggedIn = () => !!localStorage.getItem('token')
|
||||
const ensureLoggedIn = () => !isLoggedIn() && (window.location.href = '/')
|
||||
|
||||
export const USER = {
|
||||
register,
|
||||
login,
|
||||
confirmEmail: (token: string) =>
|
||||
post('/api/v2/user/confirm-email', { token }),
|
||||
me: (): Promise<AquaNetUser> => {
|
||||
ensureLoggedIn()
|
||||
return post('/api/v2/user/me', {})
|
||||
},
|
||||
keychip: (): Promise<string> =>
|
||||
post('/api/v2/user/keychip', {}).then(it => it.keychip),
|
||||
setting: (key: string, value: string) =>
|
||||
post('/api/v2/user/setting', { key: key === 'password' ? 'pwHash' : key, value }),
|
||||
uploadPfp: (file: File) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return post('/api/v2/user/upload-pfp', { }, { method: 'POST', body: formData })
|
||||
},
|
||||
isLoggedIn,
|
||||
ensureLoggedIn,
|
||||
}
|
||||
|
||||
export const CARD = {
|
||||
summary: (cardId: string): Promise<{card: Card, summary: CardSummary}> =>
|
||||
post('/api/v2/card/summary', { cardId }),
|
||||
link: (props: { cardId: string, migrate: string }) =>
|
||||
post('/api/v2/card/link', props),
|
||||
unlink: (cardId: string) =>
|
||||
post('/api/v2/card/unlink', { cardId }),
|
||||
userGames: (username: string): Promise<CardSummary> =>
|
||||
post('/api/v2/card/user-games', { username }),
|
||||
}
|
||||
|
||||
export const GAME = {
|
||||
trend: (username: string, game: GameName): Promise<TrendEntry[]> =>
|
||||
post(`/api/v2/game/${game}/trend`, { username }),
|
||||
userSummary: (username: string, game: GameName): Promise<GenericGameSummary> =>
|
||||
post(`/api/v2/game/${game}/user-summary`, { username }),
|
||||
ranking: (game: GameName): Promise<GenericRanking[]> =>
|
||||
post(`/api/v2/game/${game}/ranking`, { }),
|
||||
|
||||
}
|
||||
|
||||
export const DATA = {
|
||||
allMusic: (game: GameName): Promise<AllMusic> =>
|
||||
fetch(`${DATA_HOST}/d/${game}/00/all-music.json`).then(it => it.json())
|
||||
}
|
||||
|
||||
export const SETTING = {
|
||||
get: (): Promise<GameOption[]> =>
|
||||
post('/api/v2/settings/get', {}),
|
||||
set: (key: string, value: any) =>
|
||||
post('/api/v2/settings/set', { key, value: `${value}` }),
|
||||
}
|
||||
@@ -1,18 +1,22 @@
|
||||
import {
|
||||
CategoryScale,
|
||||
Chart as ChartJS,
|
||||
type ChartOptions,
|
||||
Legend,
|
||||
LinearScale,
|
||||
LineElement,
|
||||
PointElement,
|
||||
TimeScale,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
LineElement,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
CategoryScale, TimeScale, type ChartOptions, type LineOptions,
|
||||
} from 'chart.js'
|
||||
import moment from 'moment/moment'
|
||||
// @ts-expect-error Cal-heatmap does not have proper types
|
||||
import CalHeatmap from 'cal-heatmap'
|
||||
// @ts-expect-error Cal-heatmap does not have proper types
|
||||
import CalTooltip from 'cal-heatmap/plugins/Tooltip'
|
||||
import { AQUA_HOST, DEFAULT_PFP } from "./config";
|
||||
import type { AquaNetUser } from "./generalTypes";
|
||||
|
||||
export function title(t: string) {
|
||||
document.title = `AquaNet - ${t}`
|
||||
@@ -31,7 +35,7 @@ export function registerChart() {
|
||||
)
|
||||
}
|
||||
|
||||
export function renderCal(el: HTMLElement, d: {date: any, value: any}[]) {
|
||||
export function renderCal(el: HTMLElement, d: { date: any, value: any }[]): Promise<any> {
|
||||
const cal = new CalHeatmap()
|
||||
return cal.paint({
|
||||
itemSelector: el,
|
||||
@@ -55,12 +59,15 @@ export function renderCal(el: HTMLElement, d: {date: any, value: any}[]) {
|
||||
date: { start: moment().subtract(1, 'year').add(1, 'month').toDate() },
|
||||
theme: 'dark',
|
||||
}, [
|
||||
[ CalTooltip, { text: (_: Date, v: number, d: any) =>
|
||||
`${v ?? 'No'} songs played on ${d.format('MMMM D, YYYY')}` }]
|
||||
[ CalTooltip, {
|
||||
text: (_: Date, v: number, d: any) =>
|
||||
`${v ?? 'No'} songs played on ${d.format('MMMM D, YYYY')}`
|
||||
} ]
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
const now = moment()
|
||||
export const CHARTJS_OPT: ChartOptions<'line'> = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
@@ -85,16 +92,73 @@ export const CHARTJS_OPT: ChartOptions<'line'> = {
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
intersect: false,
|
||||
callbacks: {
|
||||
title: (tooltipItems) => {
|
||||
const date = tooltipItems[0].parsed.x;
|
||||
const diff = now.diff(date, 'days')
|
||||
return diff ? `${diff} days ago` : 'Today'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export const pfpNotFound = (e: Event) => (e.target as HTMLImageElement).src = DEFAULT_PFP
|
||||
export const coverNotFound = (e: Event) => (e.target as HTMLImageElement).src = "/assets/imgs/no_cover.jpg"
|
||||
|
||||
|
||||
/**
|
||||
* Usage: clazz({a: false, b: true}) -> "b"
|
||||
*
|
||||
* @param obj HashMap<string, boolean>
|
||||
* use:tooltip
|
||||
*/
|
||||
export function clazz(obj: { [key: string]: boolean }) {
|
||||
return Object.keys(obj).filter(k => obj[k]).join(' ')
|
||||
export function tooltip(element: HTMLElement, params: { text: string } | string) {
|
||||
// Create div if not exists
|
||||
if (!document.querySelector('.aqua-tooltip')) {
|
||||
const div = document.createElement('div')
|
||||
div.classList.add('aqua-tooltip')
|
||||
|
||||
// Initially hidden
|
||||
div.style.display = 'none'
|
||||
|
||||
document.body.appendChild(div)
|
||||
}
|
||||
|
||||
let isFocus = false
|
||||
let div: HTMLDivElement = document.querySelector('.aqua-tooltip')!
|
||||
const p = typeof params === 'string' ? { text: params } : params
|
||||
|
||||
function updatePosition(event: MouseEvent) {
|
||||
div.style.top = `${event.pageY + 10}px`;
|
||||
div.style.left = `${event.pageX - div.clientWidth / 2 + 5}px`;
|
||||
}
|
||||
|
||||
function mouseOver(event: MouseEvent) {
|
||||
if (isFocus) return
|
||||
div.textContent = p.text;
|
||||
div.style.display = ''
|
||||
updatePosition(event);
|
||||
isFocus = true;
|
||||
}
|
||||
|
||||
function mouseLeave() {
|
||||
isFocus = false
|
||||
div.style.display = 'none'
|
||||
}
|
||||
|
||||
element.addEventListener('mouseover', mouseOver);
|
||||
element.addEventListener('mouseleave', mouseLeave);
|
||||
element.addEventListener('mousemove', updatePosition);
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
element.removeEventListener('mouseover', mouseOver);
|
||||
element.removeEventListener('mouseleave', mouseLeave);
|
||||
element.removeEventListener('mousemove', updatePosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function pfp(node: HTMLImageElement, me?: AquaNetUser) {
|
||||
node.src = me?.profilePicture ? `${AQUA_HOST}/uploads/net/portrait/${me.profilePicture}` : DEFAULT_PFP
|
||||
node.onerror = e => pfpNotFound(e as Event)
|
||||
}
|
||||
|
||||
@@ -1,83 +1,90 @@
|
||||
<main id="home" class="no-margin">
|
||||
<h1>AquaNet</h1>
|
||||
<div class="btn-group">
|
||||
<button>Login</button>
|
||||
<button>Sign Up</button>
|
||||
</div>
|
||||
<script lang="ts">
|
||||
import { fade } from "svelte/transition";
|
||||
import LinkCard from "./Home/LinkCard.svelte";
|
||||
import SetupInstructions from "./Home/SetupInstructions.svelte";
|
||||
import { DISCORD_INVITE, FADE_IN, FADE_OUT } from "../libs/config";
|
||||
import { USER } from "../libs/sdk.js";
|
||||
import type { AquaNetUser } from "../libs/generalTypes";
|
||||
import StatusOverlays from "../components/StatusOverlays.svelte";
|
||||
import ActionCard from "../components/ActionCard.svelte";
|
||||
import { t } from "../libs/i18n";
|
||||
|
||||
<div class="light-pollution">
|
||||
<div class="l1"></div>
|
||||
<div class="l2"></div>
|
||||
<div class="l3"></div>
|
||||
</div>
|
||||
USER.ensureLoggedIn();
|
||||
|
||||
let me: AquaNetUser
|
||||
let error = ""
|
||||
|
||||
let tab = 0;
|
||||
let tabs = [t('home.nav.portal'), t('home.nav.link-card'), t('home.nav.game-setup')]
|
||||
|
||||
USER.me().then((m) => me = m).catch(e => error = e.message)
|
||||
</script>
|
||||
|
||||
<main class="content">
|
||||
<!-- <h2 class="outer-title"> </h2>-->
|
||||
<nav class="tabs">
|
||||
{#each tabs as t, i}
|
||||
<div class="clickable"
|
||||
class:active={tab === i}
|
||||
on:click={() => tab = i}
|
||||
on:keydown={(e) => e.key === "Enter" && (tab = i)}
|
||||
role="button" tabindex={i}>{t}
|
||||
</div>
|
||||
{/each}
|
||||
</nav>
|
||||
|
||||
{#if tab === 0}
|
||||
<div out:fade={FADE_OUT} in:fade={FADE_IN} class="action-cards">
|
||||
<ActionCard color="255, 192, 203" icon="solar:card-bold-duotone" on:click={() => tab = 1}>
|
||||
{#if me && me.cards.length > 1}
|
||||
<h3>{t('home.manage-cards')}</h3>
|
||||
<span>{t('home.manage-cards-description')}</span>
|
||||
{:else if me}
|
||||
<h3>{t('home.link-card')}</h3>
|
||||
<span>{t('home.link-cards-description')}</span>
|
||||
{/if}
|
||||
</ActionCard>
|
||||
|
||||
<ActionCard color="82, 93, 233" icon="ic:baseline-discord" on:click={() => window.location.href = DISCORD_INVITE}>
|
||||
<h3>{t('home.join-discord')}</h3>
|
||||
<span>{t('home.join-discord-description')}</span>
|
||||
</ActionCard>
|
||||
|
||||
<ActionCard on:click={() => tab = 2} icon="uil:link-alt">
|
||||
<h3>{t('home.setup')}</h3>
|
||||
<span>{t('home.setup-description')}</span>
|
||||
</ActionCard>
|
||||
</div>
|
||||
{:else if tab === 1}
|
||||
<div out:fade={FADE_OUT} in:fade={FADE_IN}>
|
||||
<LinkCard/>
|
||||
</div>
|
||||
{:else if tab === 2}
|
||||
<div out:fade={FADE_OUT} in:fade={FADE_IN}>
|
||||
<SetupInstructions/>
|
||||
</div>
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
<StatusOverlays {error} loading={!me}/>
|
||||
|
||||
<style lang="sass">
|
||||
@import "../vars"
|
||||
|
||||
#home
|
||||
color: $c-main
|
||||
position: relative
|
||||
width: 100%
|
||||
height: 100%
|
||||
padding-left: 100px
|
||||
overflow: hidden
|
||||
.tabs
|
||||
display: flex
|
||||
gap: 1rem
|
||||
|
||||
box-sizing: border-box
|
||||
div
|
||||
&.active
|
||||
color: $c-main
|
||||
|
||||
h3
|
||||
font-size: 1.3rem
|
||||
margin: 0
|
||||
|
||||
.action-cards
|
||||
display: flex
|
||||
flex-direction: column
|
||||
justify-content: center
|
||||
|
||||
margin-top: -$nav-height
|
||||
|
||||
> h1
|
||||
font-family: Quicksand, $font
|
||||
user-select: none
|
||||
|
||||
// Gap between text characters
|
||||
letter-spacing: 0.2em
|
||||
margin-top: 0
|
||||
opacity: 0.9
|
||||
|
||||
.btn-group
|
||||
display: flex
|
||||
gap: 8px
|
||||
|
||||
.light-pollution
|
||||
pointer-events: none
|
||||
opacity: 0.6
|
||||
|
||||
> div
|
||||
position: absolute
|
||||
z-index: -1
|
||||
|
||||
.l1
|
||||
left: -560px
|
||||
top: 90px
|
||||
height: 1130px
|
||||
width: 1500px
|
||||
$color: rgb(158, 110, 230)
|
||||
background: radial-gradient(50% 50% at 50% 50%, rgba($color, 0.28) 0%, rgba($color, 0) 100%)
|
||||
|
||||
.l2
|
||||
left: -200px
|
||||
top: 560px
|
||||
height: 1200px
|
||||
width: 1500px
|
||||
$color: rgb(92, 195, 250)
|
||||
background: radial-gradient(50% 50% at 50% 50%, rgba($color, 0.28) 0%, rgba($color, 0) 100%)
|
||||
|
||||
.l3
|
||||
left: -600px
|
||||
opacity: 0.7
|
||||
top: -630px
|
||||
width: 1500px
|
||||
height: 1000px
|
||||
$color: rgb(230, 110, 156)
|
||||
background: radial-gradient(50% 50% at 50% 50%, rgba($color, 0.28) 0%, rgba($color, 0) 100%)
|
||||
|
||||
@media (max-width: 500px)
|
||||
align-items: center
|
||||
padding-left: 0
|
||||
</style>
|
||||
gap: 1rem
|
||||
</style>
|
||||
|
||||
376
AquaNet/src/pages/Home/LinkCard.svelte
Normal file
376
AquaNet/src/pages/Home/LinkCard.svelte
Normal file
@@ -0,0 +1,376 @@
|
||||
<!-- Svelte 4.2.11 -->
|
||||
|
||||
<script lang="ts">
|
||||
import { fade, slide } from "svelte/transition"
|
||||
import type { Card, CardSummary, CardSummaryGame, ConfirmProps, AquaNetUser } from "../../libs/generalTypes";
|
||||
import { CARD, USER } from "../../libs/sdk";
|
||||
import moment from "moment"
|
||||
import Icon from "@iconify/svelte";
|
||||
import StatusOverlays from "../../components/StatusOverlays.svelte";
|
||||
|
||||
// State
|
||||
let state: 'ready' | 'linking-AC' | 'linking-SN' | 'loading' = "loading"
|
||||
let showConfirm: ConfirmProps | null = null
|
||||
|
||||
let error: string = ""
|
||||
let me: AquaNetUser | null = null
|
||||
let accountCardSummary: CardSummary | null = null
|
||||
|
||||
// Fetch data for current user
|
||||
const updateMe = () => USER.me().then(m => {
|
||||
me = m
|
||||
m.cards.sort((a, b) => a.registerTime < b.registerTime ? 1 : -1)
|
||||
CARD.summary(m.ghostCard.luid).then(s => accountCardSummary = s.summary)
|
||||
|
||||
// Always put the ghost card at the top
|
||||
m.cards.sort((a, b) => a.ghost ? -1 : 1)
|
||||
state = "ready"
|
||||
}).catch(e => error = e.message)
|
||||
updateMe()
|
||||
|
||||
// Data conflict overlay
|
||||
let conflictCardID: string = ""
|
||||
let conflictSummary: CardSummary | null = null
|
||||
let conflictGame: string = ""
|
||||
let conflictNew: CardSummaryGame | null = null
|
||||
let conflictOld: CardSummaryGame | null = null
|
||||
let conflictToMigrate: string[] = []
|
||||
|
||||
function setError(msg: string, type: 'AC' | 'SN') {
|
||||
type === 'AC' ? errorAC = msg : errorSN = msg
|
||||
}
|
||||
|
||||
async function doLink(id: string, migrate: string) {
|
||||
await CARD.link({cardId: id, migrate})
|
||||
await updateMe()
|
||||
state = "ready"
|
||||
}
|
||||
|
||||
async function link(type: 'AC' | 'SN') {
|
||||
if (state !== 'ready' || accountCardSummary === null) return
|
||||
state = "linking-" + type
|
||||
const id = type === 'AC' ? inputAC : inputSN
|
||||
|
||||
console.log("linking card", id)
|
||||
|
||||
// Check if this card is already linked in the account
|
||||
if (me?.cards?.some(c => formatLUID(c.luid, c.ghost).toLowerCase() === id.toLowerCase())) {
|
||||
setError("This card is already linked to your account", type)
|
||||
state = "ready"
|
||||
return
|
||||
}
|
||||
|
||||
// First, lookup the card summary
|
||||
const card = (await CARD.summary(id).catch(e => {
|
||||
// If card is not found, create a card and link it
|
||||
if (e.message === "Card not found") {
|
||||
doLink(id, "")
|
||||
return
|
||||
}
|
||||
|
||||
setError(e.message, type)
|
||||
state = "ready"
|
||||
return
|
||||
}))!
|
||||
const summary = card.summary
|
||||
|
||||
// Check if it's already linked
|
||||
if (card.card.linked) {
|
||||
setError("This card is already linked to another account", type)
|
||||
state = "ready"
|
||||
return
|
||||
}
|
||||
|
||||
// If all games in summary are null or doesn't conflict with the ghost card,
|
||||
// we can link the card directly
|
||||
if (Object.keys(summary).every(k => summary[k as keyof CardSummary] === null
|
||||
|| accountCardSummary!![k as keyof CardSummary] === null)) {
|
||||
console.log("linking card directly")
|
||||
await doLink(id, Object.keys(summary).filter(k => summary[k as keyof CardSummary] !== null).join(","))
|
||||
}
|
||||
|
||||
// For each conflicting game, ask the user if they want to migrate the data
|
||||
else {
|
||||
conflictSummary = summary
|
||||
conflictCardID = id
|
||||
await linkConflictContinue(null)
|
||||
}
|
||||
}
|
||||
|
||||
async function linkConflictContinue(choose: "old" | "new" | null) {
|
||||
if (accountCardSummary === null || conflictSummary === null) return
|
||||
console.log("linking card with migration")
|
||||
|
||||
if (choose) {
|
||||
// If old is chosen, nothing needs to be migrated
|
||||
// If new is chosen, we need to migrate the data
|
||||
if (choose === "new") {
|
||||
conflictToMigrate.push(conflictGame)
|
||||
}
|
||||
// Continue to the next card
|
||||
conflictSummary[conflictGame as keyof CardSummary] = null
|
||||
}
|
||||
|
||||
let isConflict = false
|
||||
for (const k in conflictSummary) {
|
||||
conflictNew = conflictSummary[k as keyof CardSummary]
|
||||
conflictOld = accountCardSummary[k as keyof CardSummary]
|
||||
conflictGame = k
|
||||
if (!conflictNew || !conflictOld) continue
|
||||
|
||||
isConflict = true
|
||||
break
|
||||
}
|
||||
|
||||
// If there are no longer conflicts, we can link the card
|
||||
if (!isConflict) {
|
||||
await doLink(conflictCardID, conflictToMigrate.join(","))
|
||||
|
||||
// Reset the conflict state
|
||||
linkConflictCancel()
|
||||
}
|
||||
}
|
||||
|
||||
function linkConflictCancel() {
|
||||
state = "ready"
|
||||
conflictSummary = null
|
||||
conflictCardID = ""
|
||||
conflictGame = ""
|
||||
conflictNew = null
|
||||
conflictOld = null
|
||||
conflictToMigrate = []
|
||||
}
|
||||
|
||||
async function unlink(card: Card) {
|
||||
showConfirm = {
|
||||
title: "Unlink Card",
|
||||
message: "Are you sure you want to unlink this card?",
|
||||
confirm: async () => {
|
||||
await CARD.unlink(card.luid)
|
||||
await updateMe()
|
||||
showConfirm = null
|
||||
},
|
||||
cancel: () => showConfirm = null,
|
||||
dangerous: true
|
||||
}
|
||||
}
|
||||
|
||||
// Access code input
|
||||
const inputACRegex = /^(\d{4} ){0,4}\d{0,4}$/
|
||||
let inputAC = ""
|
||||
let errorAC = ""
|
||||
|
||||
function inputACChange(e: any) {
|
||||
e = e as InputEvent
|
||||
// Add spaces to the input
|
||||
const old = inputAC
|
||||
if (e.inputType === "insertText" && inputAC.length % 5 === 4 && inputAC.length < 24)
|
||||
inputAC += " "
|
||||
inputAC = inputAC.slice(0, 24)
|
||||
if (inputAC !== old) errorAC = ""
|
||||
}
|
||||
|
||||
// Serial number input
|
||||
const inputSNRegex = /^([0-9A-Fa-f]{0,2}:){0,7}[0-9A-Fa-f]{0,2}$/
|
||||
let inputSN = ""
|
||||
let errorSN = ""
|
||||
|
||||
function inputSNChange(e: any) {
|
||||
e = e as InputEvent
|
||||
// Add colons to the input
|
||||
const old = inputSN
|
||||
if (e.inputType === "insertText" && inputSN.length % 3 === 2 && inputSN.length < 23)
|
||||
inputSN += ":"
|
||||
inputSN = inputSN.toUpperCase().slice(0, 23)
|
||||
if (inputSN !== old) errorSN = ""
|
||||
}
|
||||
|
||||
function formatLUID(luid: string, ghost: boolean = false) {
|
||||
if (ghost) return luid.slice(0, 6) + " " + luid.slice(6).match(/.{4}/g)!.join(" ")
|
||||
switch (cardType(luid)) {
|
||||
case "Felica SN":
|
||||
return BigInt(luid).toString(16).toUpperCase().padStart(16, "0").match(/.{1,2}/g)!.join(":")
|
||||
case "Access Code":
|
||||
return luid.match(/.{4}/g)!.join(" ")
|
||||
default:
|
||||
return luid
|
||||
}
|
||||
}
|
||||
|
||||
function cardType(luid: string) {
|
||||
if (luid.startsWith("00")) return "Felica SN"
|
||||
if (luid.length === 20) return "Access Code"
|
||||
if (luid.includes(":")) return "Felica SN"
|
||||
if (luid.includes(" ")) return "Access Code"
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
function isInput(e: KeyboardEvent) {
|
||||
return e.key.length === 1 && !e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="link-card">
|
||||
<h2>Your Cards</h2>
|
||||
<p>Here are the cards you have linked to your account:</p>
|
||||
|
||||
{#if me}
|
||||
<div class="existing-cards" transition:slide>
|
||||
{#each me.cards as card (card.luid)}
|
||||
<div class:ghost={card.ghost} class='existing card' transition:fade|global>
|
||||
<span class="type">{card.ghost ? "Account Card" : cardType(card.luid)}</span>
|
||||
<span class="register">Registered: {moment(card.registerTime).format("YYYY MMM DD")}</span>
|
||||
<span class="last">Last used: {moment(card.accessTime).format("YYYY MMM DD")}</span>
|
||||
<div/>
|
||||
<span class="id">{formatLUID(card.luid, card.ghost)}</span>
|
||||
{#if !card.ghost}
|
||||
<button class="icon error" on:click={() => unlink(card)}><Icon icon="tabler:trash-x-filled"/></button>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<h2>Link Card</h2>
|
||||
<p>Please enter the following information:</p>
|
||||
{#if !inputSN}
|
||||
<div out:slide={{ duration: 250 }}>
|
||||
<p>The 20-digit access code on the back of your card.
|
||||
(If it doesn't work, please try scanning your card in game and enter the access code shown on screen)</p>
|
||||
<label>
|
||||
<!-- DO NOT change the order of bind:value and on:input. Their order determines the order of reactivity -->
|
||||
<input placeholder="e.g. 5200 1234 5678 9012 3456"
|
||||
on:keydown={(e) => {
|
||||
e.key === "Enter" && link('AC')
|
||||
// Ensure key is numeric
|
||||
if (isInput(e) && !/[\d ]/.test(e.key)) e.preventDefault()
|
||||
}}
|
||||
bind:value={inputAC}
|
||||
on:input={inputACChange}
|
||||
class:error={inputAC && (!inputACRegex.test(inputAC) || errorAC)}>
|
||||
{#if inputAC.length > 0}
|
||||
<button transition:slide={{axis: 'x'}} on:click={() => {link('AC');inputAC=''}}>Link</button>
|
||||
{/if}
|
||||
</label>
|
||||
{#if errorAC}
|
||||
<p class="error" transition:slide>{errorAC}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !inputAC}
|
||||
<div out:slide={{ duration: 250 }}>
|
||||
<p>Download the NFC Tools app on your phone
|
||||
(<a href="https://play.google.com/store/apps/details?id=com.wakdev.wdnfc">Android</a> /
|
||||
<a href="https://apps.apple.com/us/app/nfc-tools/id1252962749">Apple</a>) and scan your card.
|
||||
Then, enter the Serial Number.
|
||||
</p>
|
||||
<label>
|
||||
<input placeholder="e.g. 01:2E:1A:2B:3C:4D:5E:6F"
|
||||
on:keydown={(e) => {
|
||||
e.key === "Enter" && link('SN')
|
||||
// Ensure key is hex or colon
|
||||
if (isInput(e) && !/[0-9A-Fa-f:]/.test(e.key)) e.preventDefault()
|
||||
}}
|
||||
bind:value={inputSN}
|
||||
on:input={inputSNChange}
|
||||
class:error={inputSN && (!inputSNRegex.test(inputSN) || errorSN)}>
|
||||
{#if inputSN.length > 0}
|
||||
<button transition:slide={{axis: 'x'}} on:click={() => {link('SN'); inputSN = ''}}>Link</button>
|
||||
{/if}
|
||||
</label>
|
||||
{#if errorSN}
|
||||
<p class="error" transition:slide>{errorSN}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if conflictOld && conflictNew && me}
|
||||
<div class="overlay" transition:fade>
|
||||
<div>
|
||||
<h2>Data Conflict</h2>
|
||||
<p>The card contains data for {conflictGame}, which is already present on your account.
|
||||
Please choose the data you would like to keep</p>
|
||||
<div class="conflict-cards">
|
||||
<div class="old card clickable" on:click={() => linkConflictContinue('old')}
|
||||
role="button" tabindex="0" on:keydown={e => e.key === "Enter" && linkConflictContinue('old')}>
|
||||
<span class="type">Account Card</span>
|
||||
<span>Name: {conflictOld.name}</span>
|
||||
<span>Rating: {conflictOld.rating}</span>
|
||||
<span>Last Login: {moment(conflictOld.lastLogin).format("YYYY MMM DD")}</span>
|
||||
<span class="id">{formatLUID(me.ghostCard.luid, true)}</span>
|
||||
</div>
|
||||
<div class="new card clickable" on:click={() => linkConflictContinue('new')}
|
||||
role="button" tabindex="0" on:keydown={e => e.key === "Enter" && linkConflictContinue('new')}>
|
||||
<span class="type">{cardType(conflictCardID)}</span>
|
||||
<span>Name: {conflictNew.name}</span>
|
||||
<span>Rating: {conflictNew.rating}</span>
|
||||
<span>Last Login: {moment(conflictNew.lastLogin).format("YYYY MMM DD")}</span>
|
||||
<span class="id">{conflictCardID}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="error" on:click={linkConflictCancel}>Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<StatusOverlays bind:confirm={showConfirm} bind:error={error} loading={!me} />
|
||||
</div>
|
||||
|
||||
<style lang="sass">
|
||||
@import "../../vars"
|
||||
|
||||
.link-card
|
||||
input
|
||||
width: 100%
|
||||
|
||||
label
|
||||
display: flex
|
||||
|
||||
button
|
||||
margin-left: 1rem
|
||||
|
||||
.existing-cards, .conflict-cards
|
||||
display: grid
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr))
|
||||
gap: 1rem
|
||||
|
||||
.existing.card
|
||||
min-height: 90px
|
||||
position: relative
|
||||
overflow: hidden
|
||||
|
||||
*
|
||||
white-space: nowrap
|
||||
|
||||
&.ghost
|
||||
background: rgba($c-darker, 0.8)
|
||||
|
||||
.register, .last
|
||||
opacity: 0.7
|
||||
|
||||
span:not(.type)
|
||||
font-size: 0.8rem
|
||||
|
||||
> div
|
||||
flex: 1
|
||||
|
||||
button
|
||||
position: absolute
|
||||
right: 10px
|
||||
bottom: 10px
|
||||
|
||||
.conflict-cards
|
||||
.card
|
||||
transition: $transition
|
||||
|
||||
.card:hover
|
||||
background: $c-darker
|
||||
|
||||
span:not(.type)
|
||||
font-size: 0.8rem
|
||||
|
||||
.id
|
||||
opacity: 0.7
|
||||
|
||||
</style>
|
||||
105
AquaNet/src/pages/Home/SetupInstructions.svelte
Normal file
105
AquaNet/src/pages/Home/SetupInstructions.svelte
Normal file
@@ -0,0 +1,105 @@
|
||||
<!-- Svelte 4.2.11 -->
|
||||
|
||||
<script lang="ts">
|
||||
import { fade, slide } from "svelte/transition";
|
||||
import { USER } from "../../libs/sdk";
|
||||
import type { AquaNetUser } from "../../libs/generalTypes";
|
||||
import { codeToHtml } from 'shiki'
|
||||
import { AQUA_CONNECTION, DISCORD_INVITE, FADE_IN, FADE_OUT } from "../../libs/config";
|
||||
|
||||
let user: AquaNetUser
|
||||
let keychip: string;
|
||||
let keychipCode: string;
|
||||
let getStartedRequesting = false;
|
||||
|
||||
USER.me().then((u) => {
|
||||
user = u;
|
||||
});
|
||||
|
||||
function getStarted() {
|
||||
if (getStartedRequesting) return;
|
||||
|
||||
getStartedRequesting = true;
|
||||
USER.keychip().then(k => {
|
||||
getStartedRequesting = false;
|
||||
keychip = k;
|
||||
codeToHtml(`
|
||||
[dns]
|
||||
default=${AQUA_CONNECTION}
|
||||
|
||||
[keychip]
|
||||
enable=1
|
||||
; This is your unique keychip, do not share it with anyone
|
||||
id=${keychip.slice(0, 4)}-${keychip.slice(4)}1337`.trim(), {
|
||||
lang: 'ini',
|
||||
theme: 'rose-pine',
|
||||
}).then((html) => {
|
||||
keychipCode = html;
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="setup-instructions">
|
||||
<h2>Setup Connection</h2>
|
||||
<p>
|
||||
Welcome! If you own an arcade cabinet or game setup,
|
||||
please follow the instructions below to set up the connection with AquaDX.
|
||||
</p>
|
||||
<blockquote>
|
||||
We assume that you already have the required files and can run the game (e.g. ROM and segatools)
|
||||
that come with the cabinet or game setup.
|
||||
If not, please contact the seller of your device for the required files, as we will not provide them for copyright reasons.
|
||||
</blockquote>
|
||||
|
||||
{#if user}
|
||||
<div transition:slide>
|
||||
{#if !keychip && !keychipCode}
|
||||
<div class="no-margin" out:fade={FADE_OUT}>
|
||||
<button class="emp" on:click={getStarted}>Get started</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="no-margin" in:fade={FADE_IN}>
|
||||
<p>
|
||||
Please edit your segatools.ini file and modify the following lines:
|
||||
</p>
|
||||
|
||||
<div class="code">
|
||||
{@html keychipCode}
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Then, after you restart the game, you should be able to connect to AquaDX. Please verify that the network tests are all GOOD in the test menu.
|
||||
</p>
|
||||
<p>
|
||||
If you have any questions, please ask in our <a href={DISCORD_INVITE}>Discord server</a>.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<p>Loading...</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="sass">
|
||||
.code
|
||||
overflow-x: auto
|
||||
|
||||
:global(pre.shiki)
|
||||
background-color: transparent !important
|
||||
|
||||
:global(code)
|
||||
counter-reset: step
|
||||
counter-increment: step 0
|
||||
|
||||
:global(code .line::before)
|
||||
content: counter(step)
|
||||
counter-increment: step
|
||||
width: 1rem
|
||||
margin-right: 1.5rem
|
||||
display: inline-block
|
||||
text-align: right
|
||||
color: rgba(115,138,148,.4)
|
||||
|
||||
</style>
|
||||
@@ -1,10 +1,13 @@
|
||||
<script lang="ts">
|
||||
import {data_host} from "../libs/config";
|
||||
import {getMaimaiAllMusic, getMaimai, getMult} from "../libs/maimai";
|
||||
import type {ParsedRating, Rating} from "../libs/maimaiTypes";
|
||||
import { DATA_HOST } from "../libs/config";
|
||||
import { getMaimai, getMaimaiAllMusic } from "../libs/maimai";
|
||||
import type { ParsedRating, Rating } from "../libs/maimaiTypes";
|
||||
import { getMult } from "../libs/scoring";
|
||||
import StatusOverlays from "../components/StatusOverlays.svelte";
|
||||
|
||||
export let userId: any
|
||||
userId = +userId
|
||||
let error: string | null;
|
||||
|
||||
if (!userId) console.error("No user ID provided")
|
||||
|
||||
@@ -24,7 +27,7 @@
|
||||
old: parseRating(data.userRating.ratingList),
|
||||
new: parseRating(data.userRating.newRatingList)
|
||||
}
|
||||
})
|
||||
}).catch((e) => error = e.message)
|
||||
|
||||
function parseRating(arr: Rating[]) {
|
||||
return arr.map(x => {
|
||||
@@ -36,7 +39,7 @@
|
||||
}
|
||||
|
||||
music.note = music.notes[x.level]
|
||||
const mult = getMult(x.achievement)
|
||||
const mult = getMult(x.achievement, 'mai2')
|
||||
return {
|
||||
...x,
|
||||
music: music,
|
||||
@@ -71,19 +74,19 @@
|
||||
{#each section.data as rating}
|
||||
<div class="level-{rating.level}">
|
||||
<img class="cover"
|
||||
src={`${data_host}/maimai/assetbundle/jacket_s/00${rating.musicId.toString().padStart(6, '0').substring(2)}.png`}
|
||||
src={`${DATA_HOST}/maimai/assetbundle/jacket_s/00${rating.musicId.toString().padStart(6, '0').substring(2)}.png`}
|
||||
alt="">
|
||||
|
||||
<div class="detail">
|
||||
<span class="name">{rating.music.name}</span>
|
||||
<span class="rating">
|
||||
<span>{(rating.achievement / 10000).toFixed(2)}%</span>
|
||||
<img class="rank" src={`${data_host}/maimai/sprites/rankimage/UI_GAM_Rank_${rating.rank}.png`} alt="">
|
||||
<img class="rank" src={`${DATA_HOST}/maimai/sprites/rankimage/UI_GAM_Rank_${rating.rank}.png`} alt="">
|
||||
</span>
|
||||
<span>{rating.calc.toFixed(1)}</span>
|
||||
</div>
|
||||
<img class="ver"
|
||||
src={`${data_host}/maimai/sprites/tab/title/UI_CMN_TabTitle_MaimaiTitle_Ver${rating.music.ver.toString().substring(0, 3)}.png`}
|
||||
src={`${DATA_HOST}/maimai/sprites/tab/title/UI_CMN_TabTitle_MaimaiTitle_Ver${rating.music.ver.toString().substring(0, 3)}.png`}
|
||||
alt="">
|
||||
<div class="lv">{rating.music.note.lv}</div>
|
||||
</div>
|
||||
@@ -91,6 +94,8 @@
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<StatusOverlays error={error} loading={!parsedRatings} />
|
||||
</main>
|
||||
|
||||
<style lang="sass">
|
||||
@@ -172,7 +177,7 @@
|
||||
bottom: 0
|
||||
right: 0
|
||||
padding: 5px 10px
|
||||
background: var(--lv-color)
|
||||
background: rgb(var(--lv-color))
|
||||
// Top left border radius
|
||||
border-radius: 10px 0
|
||||
|
||||
@@ -196,4 +201,4 @@
|
||||
img.ver
|
||||
height: 45px
|
||||
left: -20px
|
||||
</style>
|
||||
</style>
|
||||
|
||||
121
AquaNet/src/pages/Ranking.svelte
Normal file
121
AquaNet/src/pages/Ranking.svelte
Normal file
@@ -0,0 +1,121 @@
|
||||
<script lang="ts">
|
||||
import { title } from "../libs/ui";
|
||||
import { GAME } from "../libs/sdk";
|
||||
import type { GenericRanking } from "../libs/generalTypes";
|
||||
import StatusOverlays from "../components/StatusOverlays.svelte";
|
||||
import type { GameName } from "../libs/scoring";
|
||||
import { GAME_TITLE } from "../libs/i18n";
|
||||
import { t } from "../libs/i18n";
|
||||
|
||||
export let game: GameName = 'mai2';
|
||||
|
||||
title(`Ranking`);
|
||||
|
||||
let d: { users: GenericRanking[] };
|
||||
let error: string | null;
|
||||
Promise.all([GAME.ranking(game)])
|
||||
.then(([users]) => {
|
||||
console.log(users)
|
||||
d = { users };
|
||||
})
|
||||
.catch((e) => error = e.message);
|
||||
</script>
|
||||
|
||||
<main class="content leaderboard">
|
||||
<div class="outer-title-options">
|
||||
<h2>{t("Leaderboard.Title")}</h2>
|
||||
<nav>
|
||||
{#each Object.entries(GAME_TITLE) as [k, v]}
|
||||
<a href="/ranking/{k}" class:active={k === game}>{v}</a>
|
||||
{/each}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{#if d}
|
||||
<div class="leaderboard-container">
|
||||
<div class="lb-user">
|
||||
<span class="rank">{t("Leaderboard.Rank")}</span>
|
||||
<span class="name"></span>
|
||||
<span class="rating">{t("Leaderboard.Rating")}</span>
|
||||
<span class="accuracy">{t("Leaderboard.Accuracy")}</span>
|
||||
<span class="fc">{t("Leaderboard.FC")}</span>
|
||||
<span class="ap">{t("Leaderboard.AP")}</span>
|
||||
</div>
|
||||
{#each d.users as user, i (user.rank)}
|
||||
<div class="lb-user" class:alternate={i % 2 === 1}>
|
||||
<span class="rank">#{user.rank}</span>
|
||||
<span class="name">
|
||||
{#if user.username !== ""}
|
||||
<a href="/u/{user.username}/{game}" class:registered={!(/user\d+/.test(user.username))}>{user.name}</a>
|
||||
{:else}
|
||||
<span>{user.name}</span>
|
||||
{/if}
|
||||
</span>
|
||||
<span class="rating">{user.rating.toLocaleString()}</span>
|
||||
<span class="accuracy">{(+user.accuracy).toFixed(2)}%</span>
|
||||
<span class="fc">{user.fullCombo}</span>
|
||||
<span class="ap">{user.allPerfect}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<StatusOverlays error={error} loading={!d} />
|
||||
</main>
|
||||
|
||||
<style lang="sass">
|
||||
@import "../vars"
|
||||
|
||||
.leaderboard-container
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
.lb-user
|
||||
display: flex
|
||||
align-items: center
|
||||
justify-content: space-between
|
||||
width: 100%
|
||||
gap: 12px
|
||||
border-radius: $border-radius
|
||||
padding: 6px 12px
|
||||
box-sizing: border-box
|
||||
|
||||
> *:not(.name)
|
||||
text-align: center
|
||||
|
||||
.name
|
||||
min-width: 100px
|
||||
flex: 1
|
||||
|
||||
> a
|
||||
color: unset
|
||||
|
||||
.registered
|
||||
background: $grad-special
|
||||
color: transparent
|
||||
-webkit-background-clip: text
|
||||
background-clip: text
|
||||
|
||||
.accuracy, .rating
|
||||
width: 15%
|
||||
min-width: 45px
|
||||
|
||||
.rating
|
||||
font-weight: bold
|
||||
color: white
|
||||
|
||||
.fc, .ap
|
||||
width: 5%
|
||||
min-width: 20px
|
||||
|
||||
@media (max-width: $w-mobile)
|
||||
font-size: 0.9rem
|
||||
|
||||
.accuracy
|
||||
display: none
|
||||
|
||||
&.alternate
|
||||
background-color: $ov-light
|
||||
|
||||
|
||||
</style>
|
||||
195
AquaNet/src/pages/User/Settings.svelte
Normal file
195
AquaNet/src/pages/User/Settings.svelte
Normal file
@@ -0,0 +1,195 @@
|
||||
<!-- Svelte 4.2.11 -->
|
||||
|
||||
<script lang="ts">
|
||||
import { slide, fade } from "svelte/transition";
|
||||
import type { AquaNetUser, GameOption } from "../../libs/generalTypes";
|
||||
import { SETTING, USER } from "../../libs/sdk";
|
||||
import StatusOverlays from "../../components/StatusOverlays.svelte";
|
||||
import Icon from "@iconify/svelte";
|
||||
import { pfp } from "../../libs/ui";
|
||||
import { t, ts } from "../../libs/i18n";
|
||||
import { FADE_IN, FADE_OUT } from "../../libs/config";
|
||||
|
||||
USER.ensureLoggedIn()
|
||||
|
||||
let me: AquaNetUser;
|
||||
let error: string;
|
||||
let submitting = ""
|
||||
let tab = 0
|
||||
const tabs = [ 'profile', 'game' ]
|
||||
|
||||
const profileFields = [
|
||||
[ 'displayName', "Display Name" ],
|
||||
[ 'username', "Username" ],
|
||||
[ 'password', "Password" ],
|
||||
[ 'profileLocation', "Location" ],
|
||||
[ 'profileBio', "Bio" ],
|
||||
]
|
||||
|
||||
let gameFields: GameOption[] = []
|
||||
|
||||
// Fetch user data
|
||||
const getMe = () => Promise.all([USER.me(), SETTING.get()]).then(([m, s]) => {
|
||||
gameFields = s
|
||||
me = m
|
||||
values = profileFields.map(([field]) => me[field as keyof AquaNetUser])
|
||||
}).catch(e => error = e.message)
|
||||
getMe()
|
||||
|
||||
let values = Array(profileFields.length).fill('')
|
||||
let changed: string[] = []
|
||||
let pfpField: HTMLInputElement
|
||||
|
||||
function submit(field: string, value: string) {
|
||||
if (submitting) return
|
||||
submitting = field
|
||||
|
||||
USER.setting(field, value).then(() => {
|
||||
changed = changed.filter(c => c !== field)
|
||||
}).catch(e => error = e.message).finally(() => submitting = "")
|
||||
}
|
||||
|
||||
function submitGameOption(field: string, value: any) {
|
||||
if (submitting) return
|
||||
submitting = field
|
||||
|
||||
SETTING.set(field, value).catch(e => error = e.message).finally(() => submitting = "")
|
||||
}
|
||||
|
||||
function uploadPfp(file: File) {
|
||||
if (submitting) return
|
||||
submitting = 'profilePicture'
|
||||
|
||||
USER.uploadPfp(file).then(() => {
|
||||
me.profilePicture = file.name
|
||||
// reload
|
||||
getMe()
|
||||
}).catch(e => error = e.message).finally(() => submitting = "")
|
||||
}
|
||||
|
||||
const passwordAction = (node: HTMLInputElement, whether: boolean) => {
|
||||
if (whether) node.type = 'password'
|
||||
}
|
||||
</script>
|
||||
|
||||
<main class="content">
|
||||
<div class="outer-title-options">
|
||||
<h2>{t('settings.title')}</h2>
|
||||
<nav>
|
||||
{#each tabs as tabName, i}
|
||||
<div transition:slide={{axis: 'x'}} class:active={tab === i}
|
||||
on:click={() => tab = i} on:keydown={e => e.key === 'Enter' && (tab = i)}
|
||||
role="button" tabindex="0">
|
||||
{ts(`settings.tabs.${tabName}`)}
|
||||
</div>
|
||||
{/each}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{#if tab === 0}
|
||||
<!-- Tab 0: Profile settings -->
|
||||
<div out:fade={FADE_OUT} in:fade={FADE_IN} class="fields">
|
||||
<div class="field">
|
||||
<label for="profile-upload">Profile Picture</label>
|
||||
<div>
|
||||
{#if me && me.profilePicture}
|
||||
<div on:click={() => pfpField.click()} on:keydown={e => e.key === 'Enter' && pfpField.click()}
|
||||
role="button" tabindex="0" class="clickable">
|
||||
<img use:pfp={me} alt="Profile" />
|
||||
</div>
|
||||
{:else}
|
||||
<button on:click={() => pfpField.click()}>
|
||||
Upload New
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<input id="profile-upload" type="file" accept="image/*" style="display: none" bind:this={pfpField}
|
||||
on:change={() => pfpField.files && uploadPfp(pfpField.files[0])} />
|
||||
</div>
|
||||
|
||||
{#each profileFields as [field, name], i (field)}
|
||||
<div class="field">
|
||||
<label for={field}>{name}</label>
|
||||
<div>
|
||||
<input id={field} type="text" use:passwordAction={field === 'password'}
|
||||
bind:value={values[i]} on:input={() => changed = [...changed, field]}
|
||||
placeholder={field === 'password' ? 'Unchanged' : 'Unset'}/>
|
||||
{#if changed.includes(field) && values[i]}
|
||||
<button transition:slide={{axis: 'x'}} on:click={() => submit(field, values[i])}>
|
||||
{#if submitting === field}
|
||||
<Icon icon="line-md:loading-twotone-loop" />
|
||||
{:else}
|
||||
Save
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if tab === 1}
|
||||
<!-- Tab 1: Game settings -->
|
||||
<div out:fade={FADE_OUT} in:fade={FADE_IN} class="fields">
|
||||
{#each gameFields as field}
|
||||
<div class="field">
|
||||
{#if field.type === "Boolean"}
|
||||
<div class="bool">
|
||||
<input id={field.key} type="checkbox" bind:checked={field.value}
|
||||
on:change={() => submitGameOption(field.key, field.value)} />
|
||||
<label for={field.key}>
|
||||
<span class="name">{ts(`settings.fields.${field.key}.name`)}</span>
|
||||
<span class="desc">{ts(`settings.fields.${field.key}.desc`)}</span>
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<StatusOverlays {error} loading={!me || !!submitting} />
|
||||
</main>
|
||||
|
||||
<style lang="sass">
|
||||
@import "../../vars"
|
||||
|
||||
.fields
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 12px
|
||||
|
||||
.bool
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: 1rem
|
||||
|
||||
label
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
.desc
|
||||
opacity: 0.6
|
||||
|
||||
.field
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
label
|
||||
max-width: max-content
|
||||
|
||||
> div:not(.bool)
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: 1rem
|
||||
margin-top: 0.5rem
|
||||
|
||||
> input
|
||||
flex: 1
|
||||
|
||||
img
|
||||
max-width: 100px
|
||||
max-height: 100px
|
||||
border-radius: $border-radius
|
||||
object-fit: cover
|
||||
|
||||
</style>
|
||||
@@ -1,190 +1,279 @@
|
||||
<script lang="ts">
|
||||
import {CHARTJS_OPT, clazz, registerChart, renderCal, title} from "../libs/ui";
|
||||
import {getMaimaiAllMusic, getMaimaiTrend, getMaimaiUser, getMult} from "../libs/maimai";
|
||||
import type {MaimaiMusic, MaimaiUserPlaylog, MaimaiUserSummaryEntry} from "../libs/maimaiTypes";
|
||||
import type {TrendEntry} from "../libs/generalTypes";
|
||||
import {data_host} from "../libs/config";
|
||||
import { CHARTJS_OPT, coverNotFound, pfpNotFound, registerChart, renderCal, title, tooltip, pfp } from "../libs/ui";
|
||||
import type {
|
||||
GenericGamePlaylog,
|
||||
GenericGameSummary,
|
||||
MusicMeta,
|
||||
TrendEntry,
|
||||
AquaNetUser,
|
||||
|
||||
AllMusic
|
||||
|
||||
} from "../libs/generalTypes";
|
||||
import { DATA_HOST } from "../libs/config";
|
||||
import 'cal-heatmap/cal-heatmap.css';
|
||||
import { Line } from 'svelte-chartjs';
|
||||
import moment from "moment";
|
||||
import 'chartjs-adapter-moment';
|
||||
import { CARD, DATA, GAME, USER } from "../libs/sdk";
|
||||
import { type GameName, getMult } from "../libs/scoring";
|
||||
import StatusOverlays from "../components/StatusOverlays.svelte";
|
||||
import Icon from "@iconify/svelte";
|
||||
import { GAME_TITLE, t } from "../libs/i18n";
|
||||
import RankDetails from "../components/RankDetails.svelte";
|
||||
import MapDetails from "../components/MapDetails.svelte";
|
||||
|
||||
const TREND_DAYS = 60
|
||||
|
||||
registerChart()
|
||||
|
||||
export let userId: any;
|
||||
userId = +userId
|
||||
export let username: string;
|
||||
export let game: GameName = "mai2"
|
||||
let calElement: HTMLElement
|
||||
let error: string;
|
||||
let me: AquaNetUser
|
||||
title(`User ${username}`)
|
||||
|
||||
title(`User ${userId}`)
|
||||
const titleText = GAME_TITLE[game]
|
||||
|
||||
interface MusicAndPlay extends MaimaiMusic, MaimaiUserPlaylog {}
|
||||
interface MusicAndPlay extends MusicMeta, GenericGamePlaylog {}
|
||||
|
||||
let d: {
|
||||
user: MaimaiUserSummaryEntry,
|
||||
user: GenericGameSummary,
|
||||
trend: TrendEntry[]
|
||||
recent: MusicAndPlay[]
|
||||
} | null = null
|
||||
recent: MusicAndPlay[],
|
||||
validGames: [ string, string ][],
|
||||
} | null
|
||||
|
||||
Promise.all([
|
||||
getMaimaiUser(userId),
|
||||
getMaimaiTrend(userId),
|
||||
getMaimaiAllMusic()
|
||||
]).then(([user, trend, music]) => {
|
||||
console.log(user)
|
||||
console.log(trend)
|
||||
console.log(music)
|
||||
let allMusics: AllMusic
|
||||
let showDetailRank = false
|
||||
USER.isLoggedIn() && USER.me().then(u => me = u)
|
||||
|
||||
// Sort recent by date
|
||||
user.recent.sort((a, b) => b.userPlayDate < a.userPlayDate ? -1 : 1)
|
||||
|
||||
d = {user, trend, recent: user.recent.map(it => {return {...music[it.musicId], ...it}})}
|
||||
localStorage.setItem("tmp-user-details", JSON.stringify(d))
|
||||
renderCal(calElement, trend.map(it => {return {date: it.date, value: it.plays}}))
|
||||
})
|
||||
CARD.userGames(username).then(games => {
|
||||
if (!games[game]) {
|
||||
// Find a valid game
|
||||
const valid = Object.entries(games).filter(([g, valid]) => valid)
|
||||
if (!valid || !valid[0]) return error = t("UserHome.NoValidGame")
|
||||
window.location.href = `/u/${username}/${valid[0][0]}`
|
||||
}
|
||||
|
||||
Promise.all([
|
||||
GAME.userSummary(username, game),
|
||||
GAME.trend(username, game),
|
||||
DATA.allMusic(game),
|
||||
]).then(([user, trend, music]) => {
|
||||
console.log(user)
|
||||
console.log(trend)
|
||||
console.log(games)
|
||||
|
||||
// If game is wacca, divide all ratings by 10
|
||||
if (game === 'wacca') {
|
||||
user.rating /= 10
|
||||
trend.forEach(it => it.rating /= 10)
|
||||
user.recent.forEach(it => {
|
||||
it.beforeRating /= 10
|
||||
it.afterRating /= 10
|
||||
})
|
||||
}
|
||||
|
||||
const minDate = moment().subtract(TREND_DAYS, 'days').format("YYYY-MM-DD")
|
||||
d = {user,
|
||||
trend: trend.filter(it => it.date >= minDate && it.plays != 0),
|
||||
recent: user.recent.map(it => {return {...music[it.musicId], ...it}}),
|
||||
validGames: Object.entries(GAME_TITLE).filter(g => games[g[0] as GameName])
|
||||
}
|
||||
allMusics = music
|
||||
renderCal(calElement, trend.map(it => {return {date: it.date, value: it.plays}})).then(() => {
|
||||
// Scroll to the rightmost
|
||||
calElement.scrollLeft = calElement.scrollWidth - calElement.clientWidth
|
||||
})
|
||||
}).catch((e) => error = e.message);
|
||||
}).catch((e) => { error = e.message; console.error(e) } );
|
||||
</script>
|
||||
|
||||
<main id="user-home">
|
||||
{#if d !== null}
|
||||
<main id="user-home" class="content">
|
||||
{#if d}
|
||||
<div class="user-pfp">
|
||||
<img src={`${data_host}/maimai/assetbundle/icon/${d.user.iconId.toString().padStart(6, "0")}.png`} alt="" class="pfp">
|
||||
<h2>{d.user.name}</h2>
|
||||
<img use:pfp={d.user.aquaUser} alt="" class="pfp" on:error={pfpNotFound}>
|
||||
<div class="name-box">
|
||||
<h2>{d.user.name}</h2>
|
||||
{#if me && me.username === username}
|
||||
<a class="setting-icon clickable" use:tooltip={t("UserHome.Settings")} href="/settings">
|
||||
<Icon icon="eos-icons:rotating-gear"/>
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
<nav>
|
||||
{#each d.validGames as [g, name]}
|
||||
<a href={`/u/${username}/${g}`} class:active={game === g}>{name}</a>
|
||||
{/each}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Rating Statistics</h2>
|
||||
<h2>{titleText} {t('UserHome.Statistics')}</h2>
|
||||
<div class="scoring-info">
|
||||
<div class="chart">
|
||||
<div class="info-top">
|
||||
<div class="rating">
|
||||
<span>DX Rating</span>
|
||||
<span>{game === 'mai2' ? t("UserHome.DXRating"): t("UserHome.Rating")}</span>
|
||||
<span>{d.user.rating.toLocaleString()}</span>
|
||||
</div>
|
||||
|
||||
<div class="rank">
|
||||
<span>Server Rank</span>
|
||||
<span>#{d.user.serverRank.toLocaleString()}</span>
|
||||
<span>{t('UserHome.ServerRank')}</span>
|
||||
<span>#{+d.user.serverRank.toLocaleString() + 1}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="trend">
|
||||
<!-- ChartJS cannot be fully responsive unless there is a parent div that's independent from its size and helps it determine its size -->
|
||||
<div class="chartjs-box-reference">
|
||||
<Line data={{
|
||||
datasets: [
|
||||
{
|
||||
label: 'Rating',
|
||||
data: d.trend.map(it => {return {x: Date.parse(it.date), y: it.rating}}),
|
||||
borderColor: '#646cff',
|
||||
tension: 0.1,
|
||||
{#if d.trend.length <= 1}
|
||||
<div class="no-data">{t("UserHome.NoData", { days: TREND_DAYS })}</div>
|
||||
{:else}
|
||||
<Line data={{
|
||||
datasets: [
|
||||
{
|
||||
label: 'Rating',
|
||||
data: d.trend.map(it => {return {x: Date.parse(it.date), y: it.rating}}),
|
||||
borderColor: '#646cff',
|
||||
tension: 0.1,
|
||||
|
||||
// TODO: Set X axis span to 3 months
|
||||
}
|
||||
]
|
||||
}} options={CHARTJS_OPT} />
|
||||
// TODO: Set X axis span to 3 months
|
||||
}
|
||||
]
|
||||
}} options={CHARTJS_OPT} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-bottom">
|
||||
{#each d.user.ranks as r}
|
||||
<div>
|
||||
<span>{r.name}</span>
|
||||
<span>{r.count}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if Object.entries(d.user.detailedRanks).length > 0}
|
||||
<div class="info-bottom clickable" use:tooltip={t("UserHome.ShowRanksDetails")}
|
||||
on:click={() => showDetailRank = !showDetailRank} role="button" tabindex="0"
|
||||
on:keydown={e => e.key === "Enter" && (showDetailRank = !showDetailRank)}>
|
||||
{#each d.user.ranks as r}
|
||||
<div><span>{r.name}</span><span>{r.count}</span></div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="info-bottom">
|
||||
{#each d.user.ranks as r}
|
||||
<div><span>{r.name}</span><span>{r.count}</span></div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="other-info">
|
||||
<div class="accuracy">
|
||||
<span>Accuracy</span>
|
||||
<span>{(d.user.accuracy / 10000).toFixed(2)}%</span>
|
||||
<span>{t('UserHome.Accuracy')}</span>
|
||||
<span>{(d.user.accuracy).toFixed(2)}%</span>
|
||||
</div>
|
||||
|
||||
<div class="max-combo">
|
||||
<span>Max Combo</span>
|
||||
<span>{t("UserHome.MaxCombo")}</span>
|
||||
<span>{d.user.maxCombo}</span>
|
||||
</div>
|
||||
|
||||
<div class="full-combo">
|
||||
<span>Full Combo</span>
|
||||
<span>{t("UserHome.FullCombo")}</span>
|
||||
<span>{d.user.fullCombo}</span>
|
||||
</div>
|
||||
|
||||
<div class="all-perfect">
|
||||
<span>All Perfect</span>
|
||||
<span>{t("UserHome.AllPerfect")}</span>
|
||||
<span>{d.user.allPerfect}</span>
|
||||
</div>
|
||||
|
||||
<div class="total-dx-score">
|
||||
<span>DX Score</span>
|
||||
<span>{d.user.totalDxScore.toLocaleString()}</span>
|
||||
<span>{game === 'mai2' ? t('UserHome.DXScore') : t("UserHome.Score")}</span>
|
||||
<span>{d.user.totalScore.toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showDetailRank}<RankDetails g={d.user}/>{/if}
|
||||
|
||||
<div>
|
||||
<h2>Play Activity</h2>
|
||||
<h2>{t('UserHome.PlayActivity')}</h2>
|
||||
<div class="activity-info">
|
||||
<div id="cal-heatmap" bind:this={calElement} />
|
||||
<div class="hide-scrollbar" id="cal-heatmap" bind:this={calElement} />
|
||||
|
||||
<div class="info-bottom">
|
||||
<div class="plays">
|
||||
<span>Plays</span>
|
||||
<span>{t("UserHome.Plays")}</span>
|
||||
<span>{d.user.plays}</span>
|
||||
</div>
|
||||
|
||||
<div class="time">
|
||||
<span>Play Time</span>
|
||||
<span>{(d.user.totalPlayTime / 60 / 60).toFixed(1)} hr</span>
|
||||
<span>{t('UserHome.PlayTime')}</span>
|
||||
<span>{(d.user.totalPlayTime / 60).toFixed(1)} hr</span>
|
||||
</div>
|
||||
|
||||
<div class="first-play">
|
||||
<span>First Seen</span>
|
||||
<span>{t('UserHome.FirstSeen')}</span>
|
||||
<span>{moment(d.user.joined).format("YYYY-MM-DD")}</span>
|
||||
</div>
|
||||
|
||||
<div class="last-play">
|
||||
<span>Last Seen</span>
|
||||
<span>{t('UserHome.LastSeen')}</span>
|
||||
<span>{moment(d.user.lastSeen).format("YYYY-MM-DD")}</span>
|
||||
</div>
|
||||
|
||||
<div class="last-version">
|
||||
<span>Last Version</span>
|
||||
<span>{t('UserHome.Version')}</span>
|
||||
<span>{d.user.lastVersion}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if d.user.ratingComposition.best35}
|
||||
<div>
|
||||
<h2>B35</h2>
|
||||
<div class="b35">
|
||||
{#each d.user.ratingComposition.best35.split(",") as map}
|
||||
<div>
|
||||
<MapDetails g={map} meta={allMusics[map.split(":")[0]]} game={game}/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="recent">
|
||||
<h2>Recent Scores</h2>
|
||||
<h2>{t('UserHome.RecentScores')}</h2>
|
||||
<div class="scores">
|
||||
{#each d.recent as r, i}
|
||||
<div class={clazz({alt: i % 2 === 0})}>
|
||||
<img src={`${data_host}/maimai/assetbundle/jacket_s/00${r.musicId.toString().padStart(6, '0').substring(2)}.png`} alt="">
|
||||
<div class:alt={i % 2 === 0}>
|
||||
<img src={`${DATA_HOST}/d/${game}/music/00${r.musicId.toString().padStart(6, '0').substring(2)}.png`} alt="" on:error={coverNotFound} />
|
||||
<div class="info">
|
||||
<div>{r.name ?? t("UserHome.UnknownSong")}</div>
|
||||
<div>
|
||||
<span class="name">{r.name}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class={`lv level-${r.level}`}>{r.notes[r.level].lv}</span>
|
||||
<span class={"rank-" + ("" + getMult(r.achievement)[2])[0]}>
|
||||
<span class="rank-text">{("" + getMult(r.achievement)[2]).replace("p", "+")}</span>
|
||||
<span class={`lv level-${r.level === 10 ? 3 : r.level}`}>
|
||||
{ r.notes?.[r.level === 10 ? 0 : r.level]?.lv?.toFixed(1) ?? '-' ?? '0'}
|
||||
</span>
|
||||
<span class={`rank-${getMult(r.achievement, game)[2].toString()[0]}`}>
|
||||
<span class="rank-text">{("" + getMult(r.achievement, game)[2]).replace("p", "+")}</span>
|
||||
<span class="rank-num">{(r.achievement / 10000).toFixed(2)}%</span>
|
||||
</span>
|
||||
<span class={"dx-change " + clazz({increased: r.afterDeluxRating - r.beforeDeluxRating > 0})}>
|
||||
{r.afterDeluxRating - r.beforeDeluxRating}
|
||||
</span>
|
||||
{#if game === 'mai2' || game === 'wacca'}
|
||||
<span class:increased={r.afterRating - r.beforeRating > 0} class="dx-change">
|
||||
{(r.afterRating - r.beforeRating).toFixed(0)}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<p>Loading...</p>
|
||||
{/if}
|
||||
|
||||
<StatusOverlays {error} loading={!d} />
|
||||
</main>
|
||||
|
||||
<style lang="sass">
|
||||
@@ -193,41 +282,56 @@
|
||||
$gap: 20px
|
||||
|
||||
#user-home
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: $gap
|
||||
margin: 100px auto 0
|
||||
padding: 0 32px 32px
|
||||
min-height: 100%
|
||||
max-width: $w-max
|
||||
|
||||
background-color: rgba(black, 0.2)
|
||||
border-radius: 16px 16px 0 0
|
||||
|
||||
@media (max-width: #{$w-max + (64px) * 2})
|
||||
margin: 100px 32px 0
|
||||
padding: 0 32px 16px
|
||||
|
||||
@media (max-width: $w-mobile)
|
||||
margin: 100px 0 0
|
||||
padding: 0 32px 16px
|
||||
|
||||
.user-pfp
|
||||
display: flex
|
||||
align-items: flex-end
|
||||
gap: $gap
|
||||
margin-top: -40px
|
||||
margin-top: -72px
|
||||
position: relative
|
||||
|
||||
h2
|
||||
font-size: 2rem
|
||||
margin: 0
|
||||
white-space: nowrap
|
||||
|
||||
nav
|
||||
position: absolute
|
||||
display: flex
|
||||
flex-direction: row
|
||||
gap: 10px
|
||||
top: 4px
|
||||
right: 0
|
||||
|
||||
.setting-icon
|
||||
font-size: 1.5rem
|
||||
color: $c-main
|
||||
cursor: pointer
|
||||
display: flex
|
||||
align-items: center
|
||||
|
||||
.name-box
|
||||
flex: 1
|
||||
display: flex
|
||||
align-items: center
|
||||
justify-content: space-between
|
||||
gap: 10px
|
||||
|
||||
.pfp
|
||||
width: 100px
|
||||
height: 100px
|
||||
border-radius: 5px
|
||||
border-radius: $border-radius
|
||||
object-fit: cover
|
||||
|
||||
@media (max-width: $w-mobile)
|
||||
.user-pfp
|
||||
margin-top: -68px
|
||||
h2
|
||||
font-size: 1.5rem
|
||||
|
||||
.pfp
|
||||
width: 80px
|
||||
height: 80px
|
||||
|
||||
.info-bottom, .info-top, .other-info
|
||||
display: flex
|
||||
gap: $gap
|
||||
@@ -244,9 +348,17 @@ $gap: 20px
|
||||
letter-spacing: 0.1em
|
||||
color: $c-main
|
||||
|
||||
.info-bottom
|
||||
width: max-content
|
||||
|
||||
.info-top > div > span:last-child
|
||||
font-size: 1.5rem
|
||||
|
||||
.info-bottom
|
||||
max-width: 100%
|
||||
flex-wrap: wrap
|
||||
row-gap: 0.5rem
|
||||
|
||||
.scoring-info
|
||||
display: flex
|
||||
gap: $gap
|
||||
@@ -273,6 +385,13 @@ $gap: 20px
|
||||
> .chartjs-box-reference
|
||||
position: absolute
|
||||
inset: 0
|
||||
display: flex
|
||||
align-items: center
|
||||
justify-content: center
|
||||
|
||||
.no-data
|
||||
opacity: 0.5
|
||||
user-select: none
|
||||
|
||||
@media (max-width: $w-mobile)
|
||||
flex-direction: column
|
||||
@@ -307,6 +426,7 @@ $gap: 20px
|
||||
.info-bottom
|
||||
flex-direction: column
|
||||
gap: 0
|
||||
width: 100%
|
||||
|
||||
> div
|
||||
flex-direction: row
|
||||
@@ -322,7 +442,7 @@ $gap: 20px
|
||||
|
||||
> div.alt
|
||||
background-color: rgba(white, 0.03)
|
||||
border-radius: 10px
|
||||
border-radius: $border-radius
|
||||
|
||||
// Image and song info
|
||||
> div
|
||||
@@ -336,22 +456,27 @@ $gap: 20px
|
||||
img
|
||||
width: 50px
|
||||
height: 50px
|
||||
border-radius: 10px
|
||||
border-radius: $border-radius
|
||||
object-fit: cover
|
||||
|
||||
// Song info and score
|
||||
> div
|
||||
> div.info
|
||||
flex: 1
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
overflow: hidden
|
||||
|
||||
// Limit song name to one line
|
||||
overflow: hidden
|
||||
.name
|
||||
> div:first-child
|
||||
flex: 1
|
||||
min-width: 0
|
||||
overflow: hidden
|
||||
overflow-wrap: anywhere
|
||||
white-space: nowrap
|
||||
text-overflow: ellipsis
|
||||
white-space: nowrap
|
||||
|
||||
// Make song score and rank not wrap
|
||||
> div:last-child
|
||||
white-space: nowrap
|
||||
|
||||
@media (max-width: $w-mobile)
|
||||
flex-direction: column
|
||||
@@ -360,9 +485,18 @@ $gap: 20px
|
||||
.rank-text
|
||||
text-align: left
|
||||
|
||||
// Score and rank should be space-between on mobile
|
||||
> div:last-child
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
gap: 10px
|
||||
|
||||
.lv
|
||||
margin-right: auto
|
||||
|
||||
.rank-S
|
||||
// Gold green gradient on text
|
||||
background: linear-gradient(90deg, #ffee94, #ffb798, #ffa3e5, #ebff94)
|
||||
background: $grad-special
|
||||
-webkit-background-clip: text
|
||||
color: transparent
|
||||
|
||||
@@ -373,10 +507,11 @@ $gap: 20px
|
||||
color: #6ba6ff
|
||||
|
||||
.lv
|
||||
background: var(--lv-color)
|
||||
min-width: 30px
|
||||
text-align: center
|
||||
background: rgba(var(--lv-color), 0.6)
|
||||
padding: 0 6px
|
||||
border-radius: 10px
|
||||
opacity: 0.8
|
||||
border-radius: $border-radius
|
||||
margin-right: 10px
|
||||
|
||||
span
|
||||
@@ -385,7 +520,7 @@ $gap: 20px
|
||||
|
||||
// Vertical table-like alignment
|
||||
span.rank-text
|
||||
min-width: 30px
|
||||
min-width: 40px
|
||||
span.rank-num
|
||||
min-width: 60px
|
||||
span.dx-change
|
||||
@@ -395,4 +530,11 @@ $gap: 20px
|
||||
&:before
|
||||
content: "+"
|
||||
color: $c-good
|
||||
</style>
|
||||
|
||||
.b35
|
||||
display: grid
|
||||
// 3 columns
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr))
|
||||
gap: $gap
|
||||
|
||||
</style>
|
||||
|
||||
251
AquaNet/src/pages/Welcome.svelte
Normal file
251
AquaNet/src/pages/Welcome.svelte
Normal file
@@ -0,0 +1,251 @@
|
||||
<script lang="ts">
|
||||
import { Turnstile } from "svelte-turnstile";
|
||||
import { slide } from 'svelte/transition';
|
||||
import { TURNSTILE_SITE_KEY } from "../libs/config";
|
||||
import Icon from "@iconify/svelte";
|
||||
import { USER } from "../libs/sdk";
|
||||
import { t } from "../libs/i18n"
|
||||
|
||||
let params = new URLSearchParams(window.location.search)
|
||||
|
||||
let state = "home"
|
||||
$: isSignup = state === "signup"
|
||||
let submitting = false
|
||||
|
||||
let email = ""
|
||||
let password = ""
|
||||
let username = ""
|
||||
let turnstile = ""
|
||||
let turnstileReset: () => void | undefined;
|
||||
|
||||
let error = ""
|
||||
let verifyMsg = ""
|
||||
|
||||
if (params.get('confirm-email')) {
|
||||
state = 'verify'
|
||||
verifyMsg = t("welcome.verifying")
|
||||
submitting = true
|
||||
|
||||
// Send request to server
|
||||
USER.confirmEmail(params.get('confirm-email')!)
|
||||
.then(() => {
|
||||
verifyMsg = t('welcome.verified')
|
||||
submitting = false
|
||||
|
||||
// Clear the query param
|
||||
window.history.replaceState({}, document.title, window.location.pathname)
|
||||
})
|
||||
.catch(e => verifyMsg = t('welcome.verification-failed', { message: e.message }))
|
||||
}
|
||||
|
||||
async function submit(): Promise<any> {
|
||||
submitting = true
|
||||
|
||||
// Check if username and password are valid
|
||||
if (email === "" || password === "") {
|
||||
error = t("welcome.email-password-missing")
|
||||
return submitting = false
|
||||
}
|
||||
|
||||
if (turnstile === "") {
|
||||
// Sleep for 100ms to allow Turnstile to finish
|
||||
error = t("welcome.waiting-turnstile")
|
||||
return setTimeout(submit, 100)
|
||||
}
|
||||
|
||||
// Signup
|
||||
if (isSignup) {
|
||||
if (username === "") {
|
||||
error = t("welcome.username-missing")
|
||||
return submitting = false
|
||||
}
|
||||
|
||||
// Send request to server
|
||||
await USER.register({ username, email, password, turnstile })
|
||||
.then(() => {
|
||||
// Show verify email message
|
||||
state = 'verify'
|
||||
verifyMsg = t("welcome.verification-sent", { email })
|
||||
})
|
||||
.catch(e => {
|
||||
error = e.message
|
||||
submitting = false
|
||||
turnstileReset()
|
||||
})
|
||||
}
|
||||
else {
|
||||
// Send request to server
|
||||
await USER.login({ email, password, turnstile })
|
||||
.then(() => window.location.href = "/home")
|
||||
.catch(e => {
|
||||
if (e.message === 'Email not verified - STATE_0') {
|
||||
state = 'verify'
|
||||
verifyMsg = t("welcome.verify-state-0")
|
||||
}
|
||||
else if (e.message === 'Email not verified - STATE_1') {
|
||||
state = 'verify'
|
||||
verifyMsg = t("welcome.verify-state-1")
|
||||
}
|
||||
else if (e.message === 'Email not verified - STATE_2') {
|
||||
state = 'verify'
|
||||
verifyMsg = t("welcome.verify-state-2")
|
||||
}
|
||||
else {
|
||||
error = e.message
|
||||
submitting = false
|
||||
turnstileReset()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
submitting = false
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<main id="home" class="no-margin">
|
||||
<div>
|
||||
<h1 id="title">AquaNet</h1>
|
||||
{#if state === "home"}
|
||||
<div class="btn-group" transition:slide>
|
||||
<button on:click={() => state = 'login'}>{t('welcome.btn-login')}</button>
|
||||
<button on:click={() => state = 'signup'}>{t('welcome.btn-signup')}</button>
|
||||
</div>
|
||||
{:else if state === "login" || state === "signup"}
|
||||
<div class="login-form" transition:slide>
|
||||
{#if error}
|
||||
<span class="error">{error}</span>
|
||||
{/if}
|
||||
<div on:click={() => state = 'home'} on:keypress={() => state = 'home'}
|
||||
role="button" tabindex="0" class="clickable">
|
||||
<Icon icon="line-md:chevron-small-left" />
|
||||
<span>{t('back')}</span>
|
||||
</div>
|
||||
{#if isSignup}
|
||||
<input type="text" placeholder={t('username')} bind:value={username}>
|
||||
{/if}
|
||||
<input type="email" placeholder={t('email')} bind:value={email}>
|
||||
<input type="password" placeholder={t('password')} bind:value={password}>
|
||||
<button on:click={submit}>
|
||||
{#if submitting}
|
||||
<Icon icon="line-md:loading-twotone-loop"/>
|
||||
{:else}
|
||||
{isSignup ? t('welcome.btn-signup') : t('welcome.btn-login')}
|
||||
{/if}
|
||||
</button>
|
||||
<Turnstile siteKey={TURNSTILE_SITE_KEY} bind:reset={turnstileReset}
|
||||
on:turnstile-callback={e => console.log(turnstile = e.detail.token)}
|
||||
on:turnstile-error={_ => console.log(error = t("welcome.turnstile-error"))}
|
||||
on:turnstile-expired={_ => window.location.reload()}
|
||||
on:turnstile-timeout={_ => console.log(error = t('welcome.turnstile-timeout'))} />
|
||||
</div>
|
||||
{:else if state === "verify"}
|
||||
<div class="login-form" transition:slide>
|
||||
<span>{verifyMsg}</span>
|
||||
{#if !submitting}
|
||||
<button on:click={() => state = 'home'} transition:slide>{t('back')}</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="light-pollution">
|
||||
<div class="l1"></div>
|
||||
<div class="l2"></div>
|
||||
<div class="l3"></div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<style lang="sass">
|
||||
@import "../vars"
|
||||
|
||||
.login-form
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 8px
|
||||
width: calc(100% - 12px)
|
||||
max-width: 300px
|
||||
|
||||
div.clickable
|
||||
display: flex
|
||||
align-items: center
|
||||
|
||||
#home
|
||||
color: $c-main
|
||||
position: relative
|
||||
width: 100%
|
||||
height: 100%
|
||||
padding-left: 100px
|
||||
overflow: hidden
|
||||
background-color: black
|
||||
|
||||
box-sizing: border-box
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
justify-content: center
|
||||
|
||||
margin-top: -$nav-height
|
||||
|
||||
// Content container
|
||||
> div
|
||||
display: flex
|
||||
flex-direction: column
|
||||
align-items: flex-start
|
||||
width: max-content
|
||||
|
||||
// Switching state container
|
||||
> div
|
||||
transition: $transition
|
||||
|
||||
#title
|
||||
font-family: Quicksand, $font
|
||||
user-select: none
|
||||
|
||||
// Gap between text characters
|
||||
letter-spacing: 0.2em
|
||||
margin-top: 0
|
||||
margin-bottom: 32px
|
||||
opacity: 0.9
|
||||
|
||||
.btn-group
|
||||
display: flex
|
||||
gap: 8px
|
||||
|
||||
.light-pollution
|
||||
pointer-events: none
|
||||
opacity: 0.8
|
||||
|
||||
> div
|
||||
position: absolute
|
||||
z-index: 1
|
||||
|
||||
.l1
|
||||
left: -560px
|
||||
top: 90px
|
||||
height: 1130px
|
||||
width: 1500px
|
||||
$color: rgb(158, 110, 230)
|
||||
background: radial-gradient(50% 50% at 50% 50%, rgba($color, 0.28) 0%, rgba(0,0,0,0) 100%)
|
||||
|
||||
.l2
|
||||
left: -200px
|
||||
top: 560px
|
||||
height: 1200px
|
||||
width: 1500px
|
||||
$color: rgb(92, 195, 250)
|
||||
background: radial-gradient(50% 50% at 50% 50%, rgba($color, 0.28) 0%, rgba(0,0,0,0) 100%)
|
||||
|
||||
.l3
|
||||
left: -600px
|
||||
opacity: 0.7
|
||||
top: -630px
|
||||
width: 1500px
|
||||
height: 1000px
|
||||
$color: rgb(230, 110, 156)
|
||||
background: radial-gradient(50% 50% at 50% 50%, rgba($color, 0.28) 0%, rgba(0,0,0,0) 100%)
|
||||
|
||||
@media (max-width: 500px)
|
||||
align-items: center
|
||||
padding-left: 0
|
||||
</style>
|
||||
@@ -1,18 +1,23 @@
|
||||
$font: Quicksand, Inter, system-ui, Avenir, Helvetica, Arial, sans-serif
|
||||
$font: Quicksand, Inter, LXGW Wenkai, Microsoft YaHei, -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, Avenir, Helvetica, Arial, sans-serif
|
||||
$c-main: #b3c6ff
|
||||
$c-sub: rgba(0, 0, 0, 0.77)
|
||||
$c-good: #b3ffb9
|
||||
$c-darker: #646cff
|
||||
$c-bg: #242424
|
||||
$c-error: #ff6b6b
|
||||
$c-shadow: rgba(0, 0, 0, 0.1)
|
||||
|
||||
$ov-light: rgba(white, 0.04)
|
||||
$ov-lighter: rgba(white, 0.08)
|
||||
$ov-dark: rgba(black, 0.1)
|
||||
$ov-darker: rgba(black, 0.18)
|
||||
|
||||
$nav-height: 4rem
|
||||
$w-mobile: 560px
|
||||
$w-max: 900px
|
||||
|
||||
.level-0
|
||||
--lv-color: #6ED43E
|
||||
.level-1
|
||||
--lv-color: #F7B807
|
||||
.level-2
|
||||
--lv-color: #FF828D
|
||||
.level-3
|
||||
--lv-color: #A051DC
|
||||
$grad-special: linear-gradient(90deg, #ffee94, #ffb798, #ffa3e5, #ebff94)
|
||||
|
||||
$border-radius: 12px
|
||||
|
||||
$transition: all 0.25s
|
||||
|
||||
1
AquaNet/src/vite-env.d.ts
vendored
1
AquaNet/src/vite-env.d.ts
vendored
@@ -1,2 +1,3 @@
|
||||
/// <reference types="svelte" />
|
||||
/// <reference types="vite/client" />
|
||||
declare const APP_VERSION: string;
|
||||
@@ -4,4 +4,7 @@ import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [svelte()],
|
||||
define: {
|
||||
APP_VERSION: JSON.stringify(process.env.npm_package_version),
|
||||
},
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
# Use a multi-stage build to keep the image size small
|
||||
# Start with a Gradle image for building the project
|
||||
FROM gradle:jdk11 as builder
|
||||
FROM gradle:jdk21-alpine as builder
|
||||
|
||||
# Copy the Gradle wrapper and configuration files separately to leverage Docker cache
|
||||
COPY --chown=gradle:gradle gradlew /home/gradle/
|
||||
@@ -20,13 +20,13 @@ COPY --chown=gradle:gradle src /home/gradle/src
|
||||
RUN ./gradlew build -x test
|
||||
|
||||
# Start with a fresh image for the runtime
|
||||
FROM openjdk:11-jre-slim
|
||||
FROM eclipse-temurin:21-jre-alpine
|
||||
|
||||
# Set the deployment directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy only the built JAR from the builder image
|
||||
COPY --from=builder /home/gradle/build/libs/aqua-?.?.??.jar /app/
|
||||
COPY --from=builder /home/gradle/build/libs/AquaDX-*.jar /app/
|
||||
|
||||
# The command to run the application
|
||||
CMD java -jar aqua-*.jar
|
||||
CMD java -jar AquaDX-*.jar
|
||||
|
||||
439
LICENSE
Normal file
439
LICENSE
Normal file
@@ -0,0 +1,439 @@
|
||||
TL;DR: https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en
|
||||
|
||||
Attribution-NonCommercial-ShareAlike 4.0 International
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons Corporation ("Creative Commons") is not a law firm and
|
||||
does not provide legal services or legal advice. Distribution of
|
||||
Creative Commons public licenses does not create a lawyer-client or
|
||||
other relationship. Creative Commons makes its licenses and related
|
||||
information available on an "as-is" basis. Creative Commons gives no
|
||||
warranties regarding its licenses, any material licensed under their
|
||||
terms and conditions, or any related information. Creative Commons
|
||||
disclaims all liability for damages resulting from their use to the
|
||||
fullest extent possible.
|
||||
|
||||
Using Creative Commons Public Licenses
|
||||
|
||||
Creative Commons public licenses provide a standard set of terms and
|
||||
conditions that creators and other rights holders may use to share
|
||||
original works of authorship and other material subject to copyright
|
||||
and certain other rights specified in the public license below. The
|
||||
following considerations are for informational purposes only, are not
|
||||
exhaustive, and do not form part of our licenses.
|
||||
|
||||
Considerations for licensors: Our public licenses are
|
||||
intended for use by those authorized to give the public
|
||||
permission to use material in ways otherwise restricted by
|
||||
copyright and certain other rights. Our licenses are
|
||||
irrevocable. Licensors should read and understand the terms
|
||||
and conditions of the license they choose before applying it.
|
||||
Licensors should also secure all rights necessary before
|
||||
applying our licenses so that the public can reuse the
|
||||
material as expected. Licensors should clearly mark any
|
||||
material not subject to the license. This includes other CC-
|
||||
licensed material, or material used under an exception or
|
||||
limitation to copyright. More considerations for licensors:
|
||||
wiki.creativecommons.org/Considerations_for_licensors
|
||||
|
||||
Considerations for the public: By using one of our public
|
||||
licenses, a licensor grants the public permission to use the
|
||||
licensed material under specified terms and conditions. If
|
||||
the licensor's permission is not necessary for any reason--for
|
||||
example, because of any applicable exception or limitation to
|
||||
copyright--then that use is not regulated by the license. Our
|
||||
licenses grant only permissions under copyright and certain
|
||||
other rights that a licensor has authority to grant. Use of
|
||||
the licensed material may still be restricted for other
|
||||
reasons, including because others have copyright or other
|
||||
rights in the material. A licensor may make special requests,
|
||||
such as asking that all changes be marked or described.
|
||||
Although not required by our licenses, you are encouraged to
|
||||
respect those requests where reasonable. More considerations
|
||||
for the public:
|
||||
wiki.creativecommons.org/Considerations_for_licensees
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
|
||||
Public License
|
||||
|
||||
By exercising the Licensed Rights (defined below), You accept and agree
|
||||
to be bound by the terms and conditions of this Creative Commons
|
||||
Attribution-NonCommercial-ShareAlike 4.0 International Public License
|
||||
("Public License"). To the extent this Public License may be
|
||||
interpreted as a contract, You are granted the Licensed Rights in
|
||||
consideration of Your acceptance of these terms and conditions, and the
|
||||
Licensor grants You such rights in consideration of benefits the
|
||||
Licensor receives from making the Licensed Material available under
|
||||
these terms and conditions.
|
||||
|
||||
|
||||
Section 1 -- Definitions.
|
||||
|
||||
a. Adapted Material means material subject to Copyright and Similar
|
||||
Rights that is derived from or based upon the Licensed Material
|
||||
and in which the Licensed Material is translated, altered,
|
||||
arranged, transformed, or otherwise modified in a manner requiring
|
||||
permission under the Copyright and Similar Rights held by the
|
||||
Licensor. For purposes of this Public License, where the Licensed
|
||||
Material is a musical work, performance, or sound recording,
|
||||
Adapted Material is always produced where the Licensed Material is
|
||||
synched in timed relation with a moving image.
|
||||
|
||||
b. Adapter's License means the license You apply to Your Copyright
|
||||
and Similar Rights in Your contributions to Adapted Material in
|
||||
accordance with the terms and conditions of this Public License.
|
||||
|
||||
c. BY-NC-SA Compatible License means a license listed at
|
||||
creativecommons.org/compatiblelicenses, approved by Creative
|
||||
Commons as essentially the equivalent of this Public License.
|
||||
|
||||
d. Copyright and Similar Rights means copyright and/or similar rights
|
||||
closely related to copyright including, without limitation,
|
||||
performance, broadcast, sound recording, and Sui Generis Database
|
||||
Rights, without regard to how the rights are labeled or
|
||||
categorized. For purposes of this Public License, the rights
|
||||
specified in Section 2(b)(1)-(2) are not Copyright and Similar
|
||||
Rights.
|
||||
|
||||
e. Effective Technological Measures means those measures that, in the
|
||||
absence of proper authority, may not be circumvented under laws
|
||||
fulfilling obligations under Article 11 of the WIPO Copyright
|
||||
Treaty adopted on December 20, 1996, and/or similar international
|
||||
agreements.
|
||||
|
||||
f. Exceptions and Limitations means fair use, fair dealing, and/or
|
||||
any other exception or limitation to Copyright and Similar Rights
|
||||
that applies to Your use of the Licensed Material.
|
||||
|
||||
g. License Elements means the license attributes listed in the name
|
||||
of a Creative Commons Public License. The License Elements of this
|
||||
Public License are Attribution, NonCommercial, and ShareAlike.
|
||||
|
||||
h. Licensed Material means the artistic or literary work, database,
|
||||
or other material to which the Licensor applied this Public
|
||||
License.
|
||||
|
||||
i. Licensed Rights means the rights granted to You subject to the
|
||||
terms and conditions of this Public License, which are limited to
|
||||
all Copyright and Similar Rights that apply to Your use of the
|
||||
Licensed Material and that the Licensor has authority to license.
|
||||
|
||||
j. Licensor means the individual(s) or entity(ies) granting rights
|
||||
under this Public License.
|
||||
|
||||
k. NonCommercial means not primarily intended for or directed towards
|
||||
commercial advantage or monetary compensation. For purposes of
|
||||
this Public License, the exchange of the Licensed Material for
|
||||
other material subject to Copyright and Similar Rights by digital
|
||||
file-sharing or similar means is NonCommercial provided there is
|
||||
no payment of monetary compensation in connection with the
|
||||
exchange.
|
||||
|
||||
l. Share means to provide material to the public by any means or
|
||||
process that requires permission under the Licensed Rights, such
|
||||
as reproduction, public display, public performance, distribution,
|
||||
dissemination, communication, or importation, and to make material
|
||||
available to the public including in ways that members of the
|
||||
public may access the material from a place and at a time
|
||||
individually chosen by them.
|
||||
|
||||
m. Sui Generis Database Rights means rights other than copyright
|
||||
resulting from Directive 96/9/EC of the European Parliament and of
|
||||
the Council of 11 March 1996 on the legal protection of databases,
|
||||
as amended and/or succeeded, as well as other essentially
|
||||
equivalent rights anywhere in the world.
|
||||
|
||||
n. You means the individual or entity exercising the Licensed Rights
|
||||
under this Public License. Your has a corresponding meaning.
|
||||
|
||||
|
||||
Section 2 -- Scope.
|
||||
|
||||
a. License grant.
|
||||
|
||||
1. Subject to the terms and conditions of this Public License,
|
||||
the Licensor hereby grants You a worldwide, royalty-free,
|
||||
non-sublicensable, non-exclusive, irrevocable license to
|
||||
exercise the Licensed Rights in the Licensed Material to:
|
||||
|
||||
a. reproduce and Share the Licensed Material, in whole or
|
||||
in part, for NonCommercial purposes only; and
|
||||
|
||||
b. produce, reproduce, and Share Adapted Material for
|
||||
NonCommercial purposes only.
|
||||
|
||||
2. Exceptions and Limitations. For the avoidance of doubt, where
|
||||
Exceptions and Limitations apply to Your use, this Public
|
||||
License does not apply, and You do not need to comply with
|
||||
its terms and conditions.
|
||||
|
||||
3. Term. The term of this Public License is specified in Section
|
||||
6(a).
|
||||
|
||||
4. Media and formats; technical modifications allowed. The
|
||||
Licensor authorizes You to exercise the Licensed Rights in
|
||||
all media and formats whether now known or hereafter created,
|
||||
and to make technical modifications necessary to do so. The
|
||||
Licensor waives and/or agrees not to assert any right or
|
||||
authority to forbid You from making technical modifications
|
||||
necessary to exercise the Licensed Rights, including
|
||||
technical modifications necessary to circumvent Effective
|
||||
Technological Measures. For purposes of this Public License,
|
||||
simply making modifications authorized by this Section 2(a)
|
||||
(4) never produces Adapted Material.
|
||||
|
||||
5. Downstream recipients.
|
||||
|
||||
a. Offer from the Licensor -- Licensed Material. Every
|
||||
recipient of the Licensed Material automatically
|
||||
receives an offer from the Licensor to exercise the
|
||||
Licensed Rights under the terms and conditions of this
|
||||
Public License.
|
||||
|
||||
b. Additional offer from the Licensor -- Adapted Material.
|
||||
Every recipient of Adapted Material from You
|
||||
automatically receives an offer from the Licensor to
|
||||
exercise the Licensed Rights in the Adapted Material
|
||||
under the conditions of the Adapter's License You apply.
|
||||
|
||||
c. No downstream restrictions. You may not offer or impose
|
||||
any additional or different terms or conditions on, or
|
||||
apply any Effective Technological Measures to, the
|
||||
Licensed Material if doing so restricts exercise of the
|
||||
Licensed Rights by any recipient of the Licensed
|
||||
Material.
|
||||
|
||||
6. No endorsement. Nothing in this Public License constitutes or
|
||||
may be construed as permission to assert or imply that You
|
||||
are, or that Your use of the Licensed Material is, connected
|
||||
with, or sponsored, endorsed, or granted official status by,
|
||||
the Licensor or others designated to receive attribution as
|
||||
provided in Section 3(a)(1)(A)(i).
|
||||
|
||||
b. Other rights.
|
||||
|
||||
1. Moral rights, such as the right of integrity, are not
|
||||
licensed under this Public License, nor are publicity,
|
||||
privacy, and/or other similar personality rights; however, to
|
||||
the extent possible, the Licensor waives and/or agrees not to
|
||||
assert any such rights held by the Licensor to the limited
|
||||
extent necessary to allow You to exercise the Licensed
|
||||
Rights, but not otherwise.
|
||||
|
||||
2. Patent and trademark rights are not licensed under this
|
||||
Public License.
|
||||
|
||||
3. To the extent possible, the Licensor waives any right to
|
||||
collect royalties from You for the exercise of the Licensed
|
||||
Rights, whether directly or through a collecting society
|
||||
under any voluntary or waivable statutory or compulsory
|
||||
licensing scheme. In all other cases the Licensor expressly
|
||||
reserves any right to collect such royalties, including when
|
||||
the Licensed Material is used other than for NonCommercial
|
||||
purposes.
|
||||
|
||||
|
||||
Section 3 -- License Conditions.
|
||||
|
||||
Your exercise of the Licensed Rights is expressly made subject to the
|
||||
following conditions.
|
||||
|
||||
a. Attribution.
|
||||
|
||||
1. If You Share the Licensed Material (including in modified
|
||||
form), You must:
|
||||
|
||||
a. retain the following if it is supplied by the Licensor
|
||||
with the Licensed Material:
|
||||
|
||||
i. identification of the creator(s) of the Licensed
|
||||
Material and any others designated to receive
|
||||
attribution, in any reasonable manner requested by
|
||||
the Licensor (including by pseudonym if
|
||||
designated);
|
||||
|
||||
ii. a copyright notice;
|
||||
|
||||
iii. a notice that refers to this Public License;
|
||||
|
||||
iv. a notice that refers to the disclaimer of
|
||||
warranties;
|
||||
|
||||
v. a URI or hyperlink to the Licensed Material to the
|
||||
extent reasonably practicable;
|
||||
|
||||
b. indicate if You modified the Licensed Material and
|
||||
retain an indication of any previous modifications; and
|
||||
|
||||
c. indicate the Licensed Material is licensed under this
|
||||
Public License, and include the text of, or the URI or
|
||||
hyperlink to, this Public License.
|
||||
|
||||
2. You may satisfy the conditions in Section 3(a)(1) in any
|
||||
reasonable manner based on the medium, means, and context in
|
||||
which You Share the Licensed Material. For example, it may be
|
||||
reasonable to satisfy the conditions by providing a URI or
|
||||
hyperlink to a resource that includes the required
|
||||
information.
|
||||
3. If requested by the Licensor, You must remove any of the
|
||||
information required by Section 3(a)(1)(A) to the extent
|
||||
reasonably practicable.
|
||||
|
||||
b. ShareAlike.
|
||||
|
||||
In addition to the conditions in Section 3(a), if You Share
|
||||
Adapted Material You produce, the following conditions also apply.
|
||||
|
||||
1. The Adapter's License You apply must be a Creative Commons
|
||||
license with the same License Elements, this version or
|
||||
later, or a BY-NC-SA Compatible License.
|
||||
|
||||
2. You must include the text of, or the URI or hyperlink to, the
|
||||
Adapter's License You apply. You may satisfy this condition
|
||||
in any reasonable manner based on the medium, means, and
|
||||
context in which You Share Adapted Material.
|
||||
|
||||
3. You may not offer or impose any additional or different terms
|
||||
or conditions on, or apply any Effective Technological
|
||||
Measures to, Adapted Material that restrict exercise of the
|
||||
rights granted under the Adapter's License You apply.
|
||||
|
||||
|
||||
Section 4 -- Sui Generis Database Rights.
|
||||
|
||||
Where the Licensed Rights include Sui Generis Database Rights that
|
||||
apply to Your use of the Licensed Material:
|
||||
|
||||
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
|
||||
to extract, reuse, reproduce, and Share all or a substantial
|
||||
portion of the contents of the database for NonCommercial purposes
|
||||
only;
|
||||
|
||||
b. if You include all or a substantial portion of the database
|
||||
contents in a database in which You have Sui Generis Database
|
||||
Rights, then the database in which You have Sui Generis Database
|
||||
Rights (but not its individual contents) is Adapted Material,
|
||||
including for purposes of Section 3(b); and
|
||||
|
||||
c. You must comply with the conditions in Section 3(a) if You Share
|
||||
all or a substantial portion of the contents of the database.
|
||||
|
||||
For the avoidance of doubt, this Section 4 supplements and does not
|
||||
replace Your obligations under this Public License where the Licensed
|
||||
Rights include other Copyright and Similar Rights.
|
||||
|
||||
|
||||
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
|
||||
|
||||
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
|
||||
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
|
||||
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
|
||||
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
|
||||
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
|
||||
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
|
||||
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
|
||||
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
|
||||
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
|
||||
|
||||
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
|
||||
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
|
||||
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
|
||||
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
|
||||
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
|
||||
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
|
||||
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
|
||||
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
|
||||
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
|
||||
|
||||
c. The disclaimer of warranties and limitation of liability provided
|
||||
above shall be interpreted in a manner that, to the extent
|
||||
possible, most closely approximates an absolute disclaimer and
|
||||
waiver of all liability.
|
||||
|
||||
|
||||
Section 6 -- Term and Termination.
|
||||
|
||||
a. This Public License applies for the term of the Copyright and
|
||||
Similar Rights licensed here. However, if You fail to comply with
|
||||
this Public License, then Your rights under this Public License
|
||||
terminate automatically.
|
||||
|
||||
b. Where Your right to use the Licensed Material has terminated under
|
||||
Section 6(a), it reinstates:
|
||||
|
||||
1. automatically as of the date the violation is cured, provided
|
||||
it is cured within 30 days of Your discovery of the
|
||||
violation; or
|
||||
|
||||
2. upon express reinstatement by the Licensor.
|
||||
|
||||
For the avoidance of doubt, this Section 6(b) does not affect any
|
||||
right the Licensor may have to seek remedies for Your violations
|
||||
of this Public License.
|
||||
|
||||
c. For the avoidance of doubt, the Licensor may also offer the
|
||||
Licensed Material under separate terms or conditions or stop
|
||||
distributing the Licensed Material at any time; however, doing so
|
||||
will not terminate this Public License.
|
||||
|
||||
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
|
||||
License.
|
||||
|
||||
|
||||
Section 7 -- Other Terms and Conditions.
|
||||
|
||||
a. The Licensor shall not be bound by any additional or different
|
||||
terms or conditions communicated by You unless expressly agreed.
|
||||
|
||||
b. Any arrangements, understandings, or agreements regarding the
|
||||
Licensed Material not stated herein are separate from and
|
||||
independent of the terms and conditions of this Public License.
|
||||
|
||||
|
||||
Section 8 -- Interpretation.
|
||||
|
||||
a. For the avoidance of doubt, this Public License does not, and
|
||||
shall not be interpreted to, reduce, limit, restrict, or impose
|
||||
conditions on any use of the Licensed Material that could lawfully
|
||||
be made without permission under this Public License.
|
||||
|
||||
b. To the extent possible, if any provision of this Public License is
|
||||
deemed unenforceable, it shall be automatically reformed to the
|
||||
minimum extent necessary to make it enforceable. If the provision
|
||||
cannot be reformed, it shall be severed from this Public License
|
||||
without affecting the enforceability of the remaining terms and
|
||||
conditions.
|
||||
|
||||
c. No term or condition of this Public License will be waived and no
|
||||
failure to comply consented to unless expressly agreed to by the
|
||||
Licensor.
|
||||
|
||||
d. Nothing in this Public License constitutes or may be interpreted
|
||||
as a limitation upon, or waiver of, any privileges and immunities
|
||||
that apply to the Licensor or You, including from the legal
|
||||
processes of any jurisdiction or authority.
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons is not a party to its public
|
||||
licenses. Notwithstanding, Creative Commons may elect to apply one of
|
||||
its public licenses to material it publishes and in those instances
|
||||
will be considered the “Licensor.” The text of the Creative Commons
|
||||
public licenses is dedicated to the public domain under the CC0 Public
|
||||
Domain Dedication. Except for the limited purpose of indicating that
|
||||
material is shared under a Creative Commons public license or as
|
||||
otherwise permitted by the Creative Commons policies published at
|
||||
creativecommons.org/policies, Creative Commons does not authorize the
|
||||
use of the trademark "Creative Commons" or any other trademark or logo
|
||||
of Creative Commons without its prior written consent including,
|
||||
without limitation, in connection with any unauthorized modifications
|
||||
to any of its public licenses or any other arrangements,
|
||||
understandings, or agreements concerning use of licensed material. For
|
||||
the avoidance of doubt, this paragraph does not form part of the
|
||||
public licenses.
|
||||
|
||||
Creative Commons may be contacted at creativecommons.org.
|
||||
27
README.md
27
README.md
@@ -13,13 +13,16 @@ This is an attempt to rebuild the [original Aqua server](https://dev.s-ul.net/Ne
|
||||
|
||||
Below is a list of games supported by this server.
|
||||
|
||||
| Game | Ver | Codename | Thanks to |
|
||||
|---------------------------|------|---------------|--------------------------------------------|
|
||||
| SDHD: CHUNITHM (Chusan) | 2.16 | SUN Plus | [@rinsama](https://github.com/mxihan) |
|
||||
| SDEZ: MaiMai DX | 1.40 | BUDDiES | [@肥宅虾哥](https://github.com/FeiZhaixiage) |
|
||||
| SDED: Card Maker | 1.34 | | |
|
||||
| SBZV: Project DIVA Arcade | 7.10 | Future Tone | |
|
||||
| SDDT: O.N.G.E.K.I. | 1.39 | bright MEMORY | [@Gamer2097](https://github.com/Gamer2097) |
|
||||
| Game | Ver | Codename | Thanks to |
|
||||
|----------------------------|------|---------------|--------------------------------------------|
|
||||
| SDHD: CHUNITHM (Chusan) | 2.20 | LUMINOUS | [@rinsama](https://github.com/mxihan) |
|
||||
| SDEZ: MaiMai DX | 1.40 | BUDDiES | [@肥宅虾哥](https://github.com/FeiZhaixiage) |
|
||||
| SDED: Card Maker | 1.34 | | |
|
||||
| SBZV: Project DIVA Arcade | 7.10 | Future Tone | |
|
||||
| SDDT: O.N.G.E.K.I. | 1.39 | bright MEMORY | [@Gamer2097](https://github.com/Gamer2097) |
|
||||
| SDFE: Wacca (*ALPHA STAGE) | 3.07 | Reverse | |
|
||||
|
||||
> **News**: AquaDX just added Wacca support on Mar 29, 2024! Feel free to test it out, but expect bugs and issues.
|
||||
|
||||
Check out these docs for more information.
|
||||
* [Game specific notes](docs/game_specific_notes.md)
|
||||
@@ -32,7 +35,7 @@ Check out these docs for more information.
|
||||
|
||||
### Usage
|
||||
|
||||
1. Install [Java 17 JDK](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html)
|
||||
1. Install [Java 21 Temurin JDK](https://adoptium.net/temurin/releases/?version=21) (Please select your appropriate operating system)
|
||||
2. Download the latest `aqua-nightly.zip` from [Releases](https://github.com/hykilpikonna/AquaDX/releases).
|
||||
3. Extract the zip file to a folder.
|
||||
4. Run `java -jar aqua.jar` in the folder.
|
||||
@@ -46,7 +49,7 @@ Configuration is saved in `config/application.properties`, spring loads this fil
|
||||
|
||||
* The host and port of game title servers can be overwritten in `allnet.server.host` and `allnet.server.port`. By default it will send the same host and port the client used the request this information.
|
||||
This will be sent to the game at booting and being used by the following request.
|
||||
* You can switch to the MariaDB (or MySQL) database by commenting the Sqlite part.
|
||||
* You can switch to the MariaDB database by commenting the Sqlite part.
|
||||
* For some games, you might need to change some game-specific config entries.
|
||||
|
||||
### Building
|
||||
@@ -62,3 +65,9 @@ The `build/libs` folder will contain a jar file.
|
||||
* Dom Eori: Developer of forked Aqua server, from v0.0.17 and up
|
||||
* All devs who contribute to the [MiniMe server](https://dev.s-ul.net/djhackers/minime)
|
||||
* All contributors by merge requests, issues and other channels
|
||||
|
||||
### License: [CC By-NC-SA](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en)
|
||||
|
||||
* **Attribution** — You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.
|
||||
* **NonCommercial** — You may not use the material for commercial purposes.
|
||||
* **ShareAlike** — If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original.
|
||||
|
||||
104
build.gradle.kts
104
build.gradle.kts
@@ -1,13 +1,20 @@
|
||||
|
||||
import java.time.Instant
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
plugins {
|
||||
val ktVer = "2.0.0-Beta5"
|
||||
|
||||
java
|
||||
kotlin("plugin.lombok") version "1.9.22"
|
||||
kotlin("jvm") version "1.9.22"
|
||||
id("io.freefair.lombok") version "8.1.0"
|
||||
id("org.springframework.boot") version "2.7.11"
|
||||
kotlin("plugin.lombok") version ktVer
|
||||
kotlin("jvm") version ktVer
|
||||
kotlin("plugin.spring") version ktVer
|
||||
kotlin("plugin.serialization") version ktVer
|
||||
id("io.freefair.lombok") version "8.6"
|
||||
id("org.springframework.boot") version "3.2.3"
|
||||
id("com.github.ben-manes.versions") version "0.51.0"
|
||||
application
|
||||
}
|
||||
|
||||
apply(plugin = "io.spring.dependency-management")
|
||||
@@ -26,32 +33,79 @@ dependencies {
|
||||
}
|
||||
implementation("org.springframework.boot:spring-boot-starter-jetty")
|
||||
implementation("io.netty:netty-all")
|
||||
implementation("org.apache.commons:commons-lang3")
|
||||
implementation("org.apache.httpcomponents:httpclient")
|
||||
implementation("org.flywaydb:flyway-core")
|
||||
implementation("org.flywaydb:flyway-mysql")
|
||||
implementation("org.apache.commons:commons-lang3:3.14.0")
|
||||
implementation("org.apache.httpcomponents.client5:httpclient5")
|
||||
implementation("org.flywaydb:flyway-core:10.10.0")
|
||||
implementation("org.flywaydb:flyway-mysql:10.10.0")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-test") {
|
||||
exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
|
||||
}
|
||||
testImplementation("org.springframework.security:spring-security-test")
|
||||
|
||||
implementation("net.logstash.logback:logstash-logback-encoder:7.4")
|
||||
|
||||
// Database
|
||||
runtimeOnly("com.mysql:mysql-connector-j:8.3.0")
|
||||
runtimeOnly("org.mariadb.jdbc:mariadb-java-client:3.3.2")
|
||||
runtimeOnly("org.xerial:sqlite-jdbc:3.45.1.0")
|
||||
implementation("com.github.gwenn:sqlite-dialect:0.1.4")
|
||||
|
||||
runtimeOnly("org.mariadb.jdbc:mariadb-java-client:3.3.3")
|
||||
runtimeOnly("org.xerial:sqlite-jdbc:3.45.2.0")
|
||||
implementation("org.hibernate.orm:hibernate-core:6.4.4.Final")
|
||||
implementation("org.hibernate.orm:hibernate-community-dialects:6.4.4.Final")
|
||||
|
||||
// JSR305 for nullable
|
||||
implementation("com.google.code.findbugs:jsr305:3.0.2")
|
||||
|
||||
// Others
|
||||
implementation("commons-fileupload:commons-fileupload:1.5")
|
||||
// =============================
|
||||
// AquaNet Specific Dependencies
|
||||
// =============================
|
||||
|
||||
// Network
|
||||
implementation("io.ktor:ktor-client-core:2.3.8")
|
||||
implementation("io.ktor:ktor-client-cio:2.3.8")
|
||||
implementation("io.ktor:ktor-client-content-negotiation:2.3.8")
|
||||
implementation("io.ktor:ktor-client-encoding:2.3.8")
|
||||
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.8")
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||
|
||||
// Somehow these are needed for ktor even though they're not in the documentation
|
||||
runtimeOnly("org.reactivestreams:reactive-streams:1.0.4")
|
||||
runtimeOnly("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.8.0")
|
||||
|
||||
// Email
|
||||
implementation("org.simplejavamail:simple-java-mail:8.6.3")
|
||||
implementation("org.simplejavamail:spring-module:8.6.3")
|
||||
|
||||
// GeoIP
|
||||
implementation("com.maxmind.geoip2:geoip2:4.2.0")
|
||||
|
||||
// JWT Authentication
|
||||
implementation("io.jsonwebtoken:jjwt-api:0.12.5")
|
||||
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.5")
|
||||
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.5")
|
||||
|
||||
// Content validation
|
||||
implementation("org.apache.tika:tika-core:2.9.1")
|
||||
|
||||
// Import: DateTime Parsing
|
||||
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.0")
|
||||
|
||||
// Serialization
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
||||
|
||||
// Testing
|
||||
testImplementation("io.kotest:kotest-runner-junit5-jvm:5.8.1")
|
||||
testImplementation("io.kotest:kotest-assertions-core")
|
||||
}
|
||||
|
||||
group = "icu.samnya"
|
||||
version = "0.0.47"
|
||||
description = "Aqua Server"
|
||||
java.sourceCompatibility = JavaVersion.VERSION_17
|
||||
version = "1.0.0"
|
||||
description = "AquaDX Arcade Server"
|
||||
java.sourceCompatibility = JavaVersion.VERSION_21
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(21)
|
||||
}
|
||||
|
||||
springBoot {
|
||||
mainClass.set("icu.samnyan.aqua.EntryKt")
|
||||
}
|
||||
|
||||
val buildTime: String by extra(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z").withZone(ZoneId.of("UTC")).format(Instant.now()))
|
||||
|
||||
@@ -62,13 +116,19 @@ tasks.processResources {
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
enabled = false
|
||||
useJUnitPlatform()
|
||||
jvmArgs("-Dkotest.assertions.collection.print.size=100")
|
||||
}
|
||||
|
||||
tasks.withType<JavaCompile>() {
|
||||
tasks.withType<JavaCompile> {
|
||||
options.encoding = "UTF-8"
|
||||
}
|
||||
|
||||
tasks.withType<Javadoc>() {
|
||||
tasks.withType<Javadoc> {
|
||||
options.encoding = "UTF-8"
|
||||
}
|
||||
|
||||
tasks.getByName<Jar>("jar") {
|
||||
enabled = false
|
||||
}
|
||||
@@ -11,14 +11,20 @@ billing.server.port=8443
|
||||
## Server host & port return to client when boot up.
|
||||
## By default the same address and port from the client connection is returned.
|
||||
## Please notice most games won't work with localhost or 127.0.0.1
|
||||
#allnet.server.host=localhost
|
||||
#allnet.server.port=80
|
||||
#allnet.server.host=
|
||||
#allnet.server.port=
|
||||
## This is for some games that use shop name for in-game functions.
|
||||
## Specify the place name here if you need it. By default it is empty.
|
||||
#allnet.server.place-name=
|
||||
allnet.server.place-name=AquaDX
|
||||
## This enables client serial validation during power on request using keychip table in database.
|
||||
## Only enable this if you know what you are doing.
|
||||
#allnet.server.check-keychip=false
|
||||
allnet.server.check-keychip=false
|
||||
## Interval between keychip session clean up checks in ms. Default is 1 day.
|
||||
allnet.server.keychip-ses-clean-interval=86400000
|
||||
## Token that haven't been used for this amount of time will be removed from the database. Default is 2 days.
|
||||
allnet.server.keychip-ses-expire=172800000
|
||||
## Redirect url when someone open this game server in a browser.
|
||||
allnet.server.redirect=https://aquadx.net
|
||||
|
||||
## Http Server Port
|
||||
## Only change this if you have a reverse proxy running.
|
||||
@@ -54,17 +60,16 @@ game.ongeki.version=1.05.00
|
||||
game.ongeki.rival.rivals-max-count=10
|
||||
|
||||
## Maimai DX
|
||||
## Set this true if you are using old version of Splash network patch and have no other choice.
|
||||
## This is a dirty workaround. If enabled, you probably won't able to play other versions.
|
||||
game.maimai2.splash-old-patch=false
|
||||
## Allow users take photo as their avatar/portrait photo.
|
||||
game.maimai2.userPhoto.enable=true
|
||||
## Specify folder path that user portrait photo and its (.json) data save to.
|
||||
game.maimai2.userPhoto.picSavePath=data/userPhoto
|
||||
## When uploading user portraits, limit the divMaxLength parameter. 1 divLength is about equal to the file size of 10kb.
|
||||
## The default value is 32 (320kb), and the minimum value is 1 (10kb)
|
||||
game.maimai2.userPhoto.divMaxLength=32
|
||||
|
||||
## User upload saving paths
|
||||
paths.mai2-plays=data/upload/mai2/plays
|
||||
paths.mai2-portrait=data/upload/mai2/portrait
|
||||
paths.aqua-net-portrait=data/upload/net/portrait
|
||||
|
||||
## Logging
|
||||
spring.servlet.multipart.max-file-size=10MB
|
||||
@@ -75,22 +80,53 @@ spring.servlet.multipart.max-request-size=20MB
|
||||
########## For Sqlite ##########
|
||||
spring.datasource.driver-class-name=org.sqlite.JDBC
|
||||
spring.datasource.url=jdbc:sqlite:data/db.sqlite
|
||||
spring.jpa.properties.hibernate.dialect=org.sqlite.hibernate.dialect.SQLiteDialect
|
||||
|
||||
########## For MariaDB ##########
|
||||
#spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
|
||||
#spring.datasource.username=
|
||||
#spring.datasource.password=
|
||||
#spring.datasource.url=jdbc:mariadb://localhost:3306/insert_db_name_here?allowPublicKeyRetrieval=true&useSSL=false
|
||||
#spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MariaDB10Dialect
|
||||
#spring.datasource.hikari.maximum-pool-size=10
|
||||
|
||||
########## For MySQL ##########
|
||||
#spring.datasource.driver-class-name=com.mysql.jdbc.Driver
|
||||
#spring.datasource.username=
|
||||
#spring.datasource.password=
|
||||
#spring.datasource.url=jdbc:mysql://localhost:3306/insert_db_name_here?allowPublicKeyRetrieval=true&useSSL=false
|
||||
#spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
|
||||
#spring.datasource.hikari.maximum-pool-size=10
|
||||
################################
|
||||
## AquaNet Settings
|
||||
################################
|
||||
|
||||
## You can add any Spring Boot properties below
|
||||
## Link card limit
|
||||
aqua-net.link-card-limit=10
|
||||
|
||||
## CloudFlare Turnstile Captcha
|
||||
## This enables captcha for user registration.
|
||||
aqua-net.turnstile.enable=false
|
||||
aqua-net.turnstile.secret=1x0000000000000000000000000000000AA
|
||||
|
||||
## Email Settings
|
||||
aqua-net.email.enable=false
|
||||
aqua-net.email.senderName=AquaDX
|
||||
aqua-net.email.senderAddr=you@example.com
|
||||
aqua-net.email.webHost=aquadx.net
|
||||
simplejavamail.javaxmail.debug=false
|
||||
simplejavamail.smtp.host=smtp.production.host
|
||||
simplejavamail.smtp.port=443
|
||||
simplejavamail.transportstrategy=SMTPS
|
||||
simplejavamail.smtp.username=<username>
|
||||
simplejavamail.smtp.password=<password>
|
||||
#simplejavamail.proxy.username=<proxy_username>
|
||||
#simplejavamail.proxy.password=<proxy_password>
|
||||
|
||||
## GeoIP Settings (Powered by MaxMind GeoLite2)
|
||||
aqua-net.geoip.path=data/GeoLite2-Country.mmdb
|
||||
aqua-net.geoip.ip-header=CF-Connecting-IP
|
||||
|
||||
## JWT Authentication Settings
|
||||
aqua-net.jwt.secret="Open Sesame!"
|
||||
|
||||
## Disable debug pages
|
||||
server.error.whitelabel.enabled=false
|
||||
|
||||
## Adapter for Frontier (For testing only, please keep this disabled)
|
||||
aqua-net.frontier.enabled=false
|
||||
aqua-net.frontier.ftk=0x00
|
||||
|
||||
## OpenAI Settings (For content moderation)
|
||||
aqua-net.openai.api-key=sk-1234567890abcdef1234567890abcdef
|
||||
|
||||
@@ -4,23 +4,31 @@ services:
|
||||
app:
|
||||
build: .
|
||||
ports:
|
||||
- "8080:8080" # Replace with your application's port
|
||||
- "80:80"
|
||||
- "8443:8443"
|
||||
- "22345:22345"
|
||||
environment:
|
||||
- DB_HOST=db
|
||||
- DB_PORT=3306
|
||||
- DB_USER=root
|
||||
- DB_PASSWORD=mysecret
|
||||
- SPRING_DATASOURCE_URL=jdbc:mariadb://db:3306/main
|
||||
- SPRING_DATASOURCE_USERNAME=cat
|
||||
- SPRING_DATASOURCE_PASSWORD=meow
|
||||
- SPRING_DATASOURCE_DRIVER_CLASS_NAME=org.mariadb.jdbc.Driver
|
||||
depends_on:
|
||||
- db
|
||||
volumes:
|
||||
- ./config:/app/config
|
||||
- ./data:/app/data
|
||||
|
||||
db:
|
||||
image: mariadb:latest
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: mysecret
|
||||
MYSQL_DATABASE: myappdb
|
||||
MYSQL_USER: myappuser
|
||||
MYSQL_PASSWORD: myapppassword
|
||||
MYSQL_ROOT_PASSWORD: meow
|
||||
MYSQL_DATABASE: main
|
||||
MYSQL_USER: cat
|
||||
MYSQL_PASSWORD: meow
|
||||
ports:
|
||||
- "3306:3306"
|
||||
volumes:
|
||||
- ./db:/var/lib/mysql
|
||||
- "127.0.0.1:3369:3306"
|
||||
# There is an unfixed bug in Windows WSL2 that prevents volumes from working
|
||||
# properly with mariadb. Please uncomment this on Linux / MacOS.
|
||||
# Check https://stackoverflow.com/questions/76711550/i-get-an-error-on-mariadb-allways-i-start-my-app
|
||||
# volumes:
|
||||
# - ./data/db:/var/lib/mysql
|
||||
|
||||
93
docs/api-v2.md
Normal file
93
docs/api-v2.md
Normal file
@@ -0,0 +1,93 @@
|
||||
|
||||
|
||||
### CardController : /api/v2/card
|
||||
|
||||
Located at: [icu.samnyan.aqua.net.CardController](icu/samnyan/aqua/net/CardController.kt)
|
||||
|
||||
**/card/default-game** : Get the default game for the card.
|
||||
|
||||
* username: String
|
||||
* **Returns**: Game ID
|
||||
|
||||
**/card/link** : Bind a card to the user. This action will migrate selected data from the card to the user's ghost card.
|
||||
|
||||
* token: String
|
||||
* cardId: String
|
||||
* migrate: String
|
||||
* **Returns**: Success message
|
||||
|
||||
**/card/summary** : Get a summary of the card, including the user's name, rating, and last login date.
|
||||
|
||||
* cardId: String
|
||||
* **Returns**: Summary of the card
|
||||
|
||||
**/card/unlink** : Unbind a card from the user. No data will be migrated during this action.
|
||||
|
||||
* token: String
|
||||
* cardId: String
|
||||
* **Returns**: Success message
|
||||
|
||||
|
||||
### Frontier : /api/v2/frontier
|
||||
|
||||
Located at: [icu.samnyan.aqua.net.Frontier](icu/samnyan/aqua/net/Frontier.kt)
|
||||
|
||||
**/frontier/lookup-card** : Lookup a card by access code
|
||||
|
||||
* ftk: String
|
||||
* accessCode: String
|
||||
* **Returns**: Card information
|
||||
|
||||
**/frontier/register-card** : Register a new card by access code
|
||||
|
||||
* ftk: String
|
||||
* accessCode: String
|
||||
* **Returns**: Card information
|
||||
|
||||
|
||||
### UserRegistrar : /api/v2/user
|
||||
|
||||
Located at: [icu.samnyan.aqua.net.UserRegistrar](icu/samnyan/aqua/net/UserRegistrar.kt)
|
||||
|
||||
**/user/confirm-email** : Confirm email address with a token sent through email to the user.
|
||||
|
||||
* token: String
|
||||
* **Returns**: Success message
|
||||
|
||||
**/user/me** : Get the information of the current logged-in user.
|
||||
|
||||
* token: String
|
||||
* **Returns**: User information
|
||||
|
||||
**/user/login** : Login with email/username and password. This will also check if the email is verified and send another confirmation
|
||||
|
||||
* email: String
|
||||
* password: String
|
||||
* turnstile: String
|
||||
* **Returns**: JWT token
|
||||
|
||||
**/user/register** : Register a new user. This will also create a ghost card for the user and send a confirmation email.
|
||||
|
||||
* username: String
|
||||
* email: String
|
||||
* password: String
|
||||
* turnstile: String
|
||||
* **Returns**: Success message
|
||||
|
||||
**/user/setting** : Validate and set a user setting field.
|
||||
|
||||
* token: String
|
||||
* key: String
|
||||
* value: String
|
||||
* **Returns**: Success message
|
||||
|
||||
**/user/keychip** : Get a Keychip ID so that the user can connect to the server.
|
||||
|
||||
* token: String
|
||||
* **Returns**: Success message
|
||||
|
||||
**/user/upload-pfp** : Upload a profile picture for the user.
|
||||
|
||||
* token: String
|
||||
* file: MultipartFile
|
||||
* **Returns**: Success message
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,7 @@ This document is for detailed game specific notes, if any.
|
||||
|-------------------|---------|--------------------------|-------------------------|--------------------|----------------|
|
||||
| Chunithm (Chusan) | SDHD | Sun | A152 | Yes | Yes |
|
||||
| Chunithm | SDBT | Paradise Lost | A032 | Yes | Yes (Paradise) |
|
||||
| Maimai DX | SDEZ | Festival | F061 | Yes | Yes |
|
||||
| Maimai DX | SDEZ | Buddies | H061 | Yes | Yes |
|
||||
| O.N.G.E.K.I | SDDT | Bright memory | A108 | Yes | Yes |
|
||||
| Card Maker | SDED | 1.34 | A030 | Yes | Yes |
|
||||
| Maimai | SDEY | Finale | ? | No | ? |
|
||||
|
||||
83
docs/mysql_to_mariadb.md
Normal file
83
docs/mysql_to_mariadb.md
Normal file
@@ -0,0 +1,83 @@
|
||||
## Migrating MySQL to MariaDB
|
||||
|
||||
Since AquaDX 1.0.0, MySQL will no longer be supported.
|
||||
|
||||
If you were using Aqua / AquaDX <= 0.0.47 with a MySQL database before this upgrade, please follow the instructions below to migrate your data to MariaDB.
|
||||
|
||||
### 1. Run a MariaDB Server
|
||||
|
||||
To migrate, you first need to run a MariaDB server using your preferred method.
|
||||
If you don't have a preference, we recommend running it using Docker Compose.
|
||||
|
||||
Below is an example `docker-compose.yml` configuration to run a MariaDB server.
|
||||
|
||||
```yaml
|
||||
version: '3.1'
|
||||
|
||||
services:
|
||||
mariadb:
|
||||
image: mariadb
|
||||
restart: always
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: example
|
||||
MYSQL_DATABASE: aqua
|
||||
MYSQL_USER: aqua
|
||||
MYSQL_PASSWORD: aqua
|
||||
ports:
|
||||
- '127.0.0.1:3306:3306'
|
||||
volumes:
|
||||
- ./db:/var/lib/mysql
|
||||
```
|
||||
|
||||
### 2. Export Data from MySQL
|
||||
|
||||
Run the following command to export your data from MySQL. Please fill in the fields in the curly braces with your MySQL database details.
|
||||
|
||||
To prevent charset encoding issues, please run the following command on Linux / macOS.
|
||||
For some reason Windows is still not using UTF-8 by default in 2024.
|
||||
|
||||
```bash
|
||||
mysqldump --user={username} --password={password} --host={host} --port={port} {database} > aqua.sql
|
||||
```
|
||||
|
||||
Now `aqua.sql` should be created. Please open `aqua.sql` and check if `utf8mb4_0900_ai_ci` exists.
|
||||
If it does, you're running a higher version of MySQL, please replace `utf8mb4_0900_ai_ci` with `utf8mb4_general_ci` in the `aqua.sql` file.
|
||||
|
||||
```bash
|
||||
# A command to replace the strings as mentioned above (Linux / macOS only).
|
||||
sed -i 's/utf8mb4_0900_ai_ci/utf8mb4_general_ci/g' aqua.sql
|
||||
```
|
||||
|
||||
### 3. Import Data to MariaDB
|
||||
|
||||
Run the following command to import your data to MariaDB. Please fill in the fields in the curly braces with your MariaDB database details.
|
||||
|
||||
First, login to your MariaDB server.
|
||||
|
||||
```bash
|
||||
mysql --user={username} --password={password} --host={host} --port={port} {database}
|
||||
```
|
||||
|
||||
Then, import the data.
|
||||
|
||||
```sql
|
||||
source aqua.sql;
|
||||
```
|
||||
|
||||
Finally, we need to fix a flyway checksum. Aqua uses Flyway to manage database schema migrations. Most migrations were identical, but one migration used different case for MySQL and MariaDB, so we need to fix its checksum.
|
||||
|
||||
Run the following SQL query to fix the checksum.
|
||||
|
||||
```sql
|
||||
UPDATE main.flyway_schema_history t
|
||||
SET t.checksum = 357127209
|
||||
WHERE t.installed_rank = 144;
|
||||
```
|
||||
|
||||
### 5. Update AquaDX Configuration
|
||||
|
||||
Finally, update your AquaDX configuration `application.properties` to use MariaDB instead of MySQL.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
* This file was generated by the Gradle 'init' task.
|
||||
*/
|
||||
|
||||
rootProject.name = "aqua"
|
||||
rootProject.name = "AquaDX"
|
||||
|
||||
@@ -1,7 +1,184 @@
|
||||
package ext
|
||||
|
||||
import icu.samnyan.aqua.net.utils.ApiException
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.engine.cio.*
|
||||
import io.ktor.client.plugins.contentnegotiation.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.apache.tika.Tika
|
||||
import org.apache.tika.mime.MimeTypes
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.http.HttpHeaders
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
import org.springframework.http.ResponseEntity.BodyBuilder
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import java.lang.reflect.Field
|
||||
import java.nio.file.Path
|
||||
import java.security.MessageDigest
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.*
|
||||
import kotlin.reflect.KCallable
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KMutableProperty1
|
||||
import kotlin.reflect.full.isSubclassOf
|
||||
import kotlin.reflect.full.memberProperties
|
||||
import kotlin.reflect.jvm.jvmErasure
|
||||
|
||||
// Make it easier to throw a ResponseStatusException
|
||||
operator fun HttpStatus.invoke(message: String? = null): Nothing = throw ResponseStatusException(this, message ?: this.reasonPhrase)
|
||||
typealias RP = RequestParam
|
||||
typealias RB = RequestBody
|
||||
typealias RH = RequestHeader
|
||||
typealias PV = PathVariable
|
||||
typealias API = RequestMapping
|
||||
typealias Var<T, V> = KMutableProperty1<T, V>
|
||||
typealias Str = String
|
||||
typealias Bool = Boolean
|
||||
typealias JavaSerializable = java.io.Serializable
|
||||
|
||||
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY, AnnotationTarget.PROPERTY_GETTER)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class Doc(
|
||||
val desc: String,
|
||||
val ret: String = ""
|
||||
)
|
||||
|
||||
@Target(AnnotationTarget.PROPERTY)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class SettingField
|
||||
|
||||
// Reflection
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T : Any> KClass<T>.vars() = memberProperties.mapNotNull { it as? Var<T, Any> }
|
||||
fun <T : Any> KClass<T>.varsMap() = vars().associateBy { it.name }
|
||||
fun <T : Any> KClass<T>.getters() = java.methods.filter { it.name.startsWith("get") }
|
||||
fun <T : Any> KClass<T>.gettersMap() = getters().associateBy { it.name.removePrefix("get").decapitalize() }
|
||||
infix fun KCallable<*>.returns(type: KClass<*>) = returnType.jvmErasure.isSubclassOf(type)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <C, T: Any> Var<C, T>.setCast(obj: C, value: String) = set(obj, when (returnType.classifier) {
|
||||
String::class -> value
|
||||
Int::class -> value.toInt()
|
||||
Boolean::class -> value.toBoolean()
|
||||
else -> 400 - "Invalid field type $returnType"
|
||||
} as T)
|
||||
inline fun <reified T: Any> Field.gets(obj: Any): T? = get(obj)?.let { it as T }
|
||||
|
||||
// HTTP
|
||||
operator fun HttpStatus.invoke(message: String? = null): Nothing = throw ApiException(value(), message ?: this.reasonPhrase)
|
||||
operator fun Int.minus(message: String): Nothing {
|
||||
ApiException.log.info("> Error $this: $message")
|
||||
throw ApiException(this, message)
|
||||
}
|
||||
fun <R> parsing(block: () -> R) = try { block() }
|
||||
catch (e: ApiException) { throw e }
|
||||
catch (e: Exception) { 400 - e.message.toString() }
|
||||
fun BodyBuilder.headers(vararg pairs: Pair<String, String>) = headers(HttpHeaders().apply { pairs.forEach { (k, v) -> set(k, v) } })
|
||||
|
||||
// Email validation
|
||||
// https://www.baeldung.com/java-email-validation-regex
|
||||
val emailRegex = "^(?=.{1,64}@)[\\p{L}0-9_-]+(\\.[\\p{L}0-9_-]+)*@[^-][\\p{L}0-9-]+(\\.[\\p{L}0-9-]+)*(\\.[\\p{L}]{2,})$".toRegex()
|
||||
fun Str.isValidEmail(): Bool = emailRegex.matches(this)
|
||||
|
||||
// Global Tools
|
||||
val HTTP = HttpClient(CIO) {
|
||||
install(ContentNegotiation) {
|
||||
json(JSON)
|
||||
}
|
||||
}
|
||||
val TIKA = Tika()
|
||||
val MIMES = MimeTypes.getDefaultMimeTypes()
|
||||
val MD5 = MessageDigest.getInstance("MD5")
|
||||
|
||||
// Class resource
|
||||
object Ext { val log = logger() }
|
||||
fun res(name: Str) = Ext::class.java.getResourceAsStream(name)
|
||||
fun resStr(name: Str) = res(name)?.reader()?.readText()
|
||||
inline fun <reified T> resJson(name: Str, warn: Boolean = true) = resStr(name)?.let {
|
||||
JSON.decodeFromString<T>(it)
|
||||
} ?: run { if (warn) Ext.log.warn("Resource $name is not found"); null }
|
||||
|
||||
// Date and time
|
||||
fun millis() = System.currentTimeMillis()
|
||||
val DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd")
|
||||
fun LocalDate.isoDate() = format(DATE_FORMAT)
|
||||
fun String.isoDate() = DATE_FORMAT.parse(this, LocalDate::from)
|
||||
fun Date.utc() = toInstant().atZone(java.time.ZoneOffset.UTC).toLocalDate()
|
||||
fun LocalDate.toDate() = Date(atStartOfDay().toInstant(java.time.ZoneOffset.UTC).toEpochMilli())
|
||||
fun LocalDateTime.isoDateTime() = format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
|
||||
fun String.isoDateTime() = LocalDateTime.parse(this, DateTimeFormatter.ISO_LOCAL_DATE_TIME)
|
||||
val URL_SAFE_DT = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss")
|
||||
fun LocalDateTime.urlSafeStr() = format(URL_SAFE_DT)
|
||||
|
||||
val ALT_DATETIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
|
||||
fun Str.asDateTime() = try { LocalDateTime.parse(this, DateTimeFormatter.ISO_LOCAL_DATE_TIME) }
|
||||
catch (e: Exception) { try { LocalDateTime.parse(this, ALT_DATETIME_FORMAT) }
|
||||
catch (e: Exception) { null } }
|
||||
|
||||
val Calendar.year get() = get(Calendar.YEAR)
|
||||
val Calendar.month get() = get(Calendar.MONTH) + 1
|
||||
val Calendar.day get() = get(Calendar.DAY_OF_MONTH)
|
||||
fun cal() = Calendar.getInstance()
|
||||
fun Date.cal() = Calendar.getInstance().apply { time = this@cal }
|
||||
operator fun Calendar.invoke(field: Int) = get(field)
|
||||
val Date.sec get() = time / 1000
|
||||
|
||||
// Encodings
|
||||
fun Long.toHex(len: Int = 16): Str = "0x${this.toString(len).padStart(len, '0').uppercase()}"
|
||||
fun Map<String, Any>.toUrl() = entries.joinToString("&") { (k, v) -> "$k=$v" }
|
||||
|
||||
fun Any.long() = when (this) {
|
||||
is Boolean -> if (this) 1L else 0
|
||||
is Number -> toLong()
|
||||
is String -> toLong()
|
||||
else -> 400 - "Invalid number: $this"
|
||||
}
|
||||
fun Any.int() = long().toInt()
|
||||
operator fun Bool.unaryPlus() = if (this) 1 else 0
|
||||
|
||||
// Collections
|
||||
fun <T> ls(vararg args: T) = args.toList()
|
||||
operator fun <K, V> Map<K, V>.plus(map: Map<K, V>) =
|
||||
(if (this is MutableMap) this else toMutableMap()).apply { putAll(map) }
|
||||
operator fun <K, V> MutableMap<K, V>.plusAssign(map: Map<K, V>) { putAll(map) }
|
||||
fun <K, V: Any> Map<K, V?>.vNotNull(): Map<K, V> = filterValues { it != null }.mapValues { it.value!! }
|
||||
fun <T> MutableList<T>.popAll(list: List<T>) = list.also { removeAll(it) }
|
||||
fun <T> MutableList<T>.popAll(vararg items: T) = popAll(items.toList())
|
||||
inline fun <T> Iterable<T>.mapApply(block: T.() -> Unit) = map { it.apply(block) }
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <K, V: Any> Map<K, V?>.recursiveNotNull(): Map<K, V> = mapNotNull { (k, v) ->
|
||||
k to if (v is Map<*, *>) (v as Map<Any?, Any?>).recursiveNotNull() else v
|
||||
}.toMap() as Map<K, V>
|
||||
|
||||
// Optionals
|
||||
operator fun <T> Optional<T>.invoke(): T? = orElse(null)
|
||||
|
||||
// Strings
|
||||
operator fun Str.get(range: IntRange) = substring(range.first, (range.last + 1).coerceAtMost(length))
|
||||
operator fun Str.get(start: Int, end: Int) = substring(start, end.coerceAtMost(length))
|
||||
fun Str.center(width: Int, padChar: Char = ' ') = padStart((length + width) / 2, padChar).padEnd(width, padChar)
|
||||
fun Str.splitLines() = replace("\r\n", "\n").split('\n')
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
fun Str.md5() = MD5.digest(toByteArray(Charsets.UTF_8)).toHexString()
|
||||
|
||||
// Coroutine
|
||||
suspend fun <T> async(block: suspend kotlinx.coroutines.CoroutineScope.() -> T): T = withContext(Dispatchers.IO) { block() }
|
||||
|
||||
// Paths
|
||||
fun path(part1: Str, vararg parts: Str) = Path.of(part1, *parts)
|
||||
fun Str.path() = Path.of(this)
|
||||
operator fun Path.div(part: Str) = resolve(part)
|
||||
fun Str.ensureEndingSlash() = if (endsWith('/')) this else "$this/"
|
||||
|
||||
fun <T: Any> T.logger() = LoggerFactory.getLogger(this::class.java)
|
||||
|
||||
// I hate this ;-;
|
||||
operator fun <E> List<E>.component6(): E = get(5)
|
||||
operator fun <E> List<E>.component7(): E = get(6)
|
||||
operator fun <E> List<E>.component8(): E = get(7)
|
||||
operator fun <E> List<E>.component9(): E = get(8)
|
||||
operator fun <E> List<E>.component10(): E = get(9)
|
||||
operator fun <E> List<E>.component11(): E = get(10)
|
||||
operator fun <E> List<E>.component12(): E = get(11)
|
||||
operator fun <E> List<E>.component13(): E = get(12)
|
||||
|
||||
74
src/main/java/ext/Json.kt
Normal file
74
src/main/java/ext/Json.kt
Normal file
@@ -0,0 +1,74 @@
|
||||
package ext
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.databind.*
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonNamingStrategy
|
||||
|
||||
// Jackson
|
||||
val ACCEPTABLE_FALSE = setOf("0", "false", "no", "off", "False", "None", "null")
|
||||
val ACCEPTABLE_TRUE = setOf("1", "true", "yes", "on", "True")
|
||||
val JSON_FUZZY_BOOLEAN = SimpleModule().addDeserializer(Boolean::class.java, object : JsonDeserializer<Boolean>() {
|
||||
override fun deserialize(parser: JsonParser, context: DeserializationContext) = when(parser.text) {
|
||||
in ACCEPTABLE_FALSE -> false
|
||||
in ACCEPTABLE_TRUE -> true
|
||||
else -> 400 - "Invalid boolean value ${parser.text}"
|
||||
}
|
||||
})
|
||||
val JSON_DATETIME = SimpleModule().addDeserializer(java.time.LocalDateTime::class.java, object : JsonDeserializer<java.time.LocalDateTime>() {
|
||||
override fun deserialize(parser: JsonParser, context: DeserializationContext) =
|
||||
parser.text.asDateTime() ?: (400 - "Invalid date time value ${parser.text}")
|
||||
})
|
||||
val JACKSON = jacksonObjectMapper().apply {
|
||||
setSerializationInclusion(JsonInclude.Include.NON_NULL)
|
||||
setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL)
|
||||
findAndRegisterModules()
|
||||
registerModule(JSON_FUZZY_BOOLEAN)
|
||||
registerModule(JSON_DATETIME)
|
||||
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||
setPropertyNamingStrategy(PropertyNamingStrategy.LOWER_CAMEL_CASE);
|
||||
}
|
||||
inline fun <reified T> ObjectMapper.parse(str: Str) = readValue(str, T::class.java)
|
||||
inline fun <reified T> ObjectMapper.parse(map: Map<*, *>) = convertValue(map, T::class.java)
|
||||
// TODO: https://stackoverflow.com/q/78197784/7346633
|
||||
fun <T> Str.parseJackson(cls: Class<T>) = if (contains("null")) {
|
||||
val map = JACKSON.parse<MutableMap<String, Any>>(this)
|
||||
JACKSON.convertValue(map.recursiveNotNull(), cls)
|
||||
}
|
||||
else JACKSON.readValue(this, cls)
|
||||
fun <T> T.toJson() = JACKSON.writeValueAsString(this)
|
||||
|
||||
inline fun <reified T> String.json() = try {
|
||||
JACKSON.readValue(this, T::class.java)
|
||||
}
|
||||
catch (e: Exception) {
|
||||
println("Failed to parse JSON: $this")
|
||||
throw e
|
||||
}
|
||||
|
||||
fun String.jsonMap(): Map<String, Any?> = json()
|
||||
fun String.jsonArray(): List<Map<String, Any?>> = json()
|
||||
|
||||
|
||||
// KotlinX Serialization
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
val JSON = Json {
|
||||
ignoreUnknownKeys = true
|
||||
isLenient = true
|
||||
namingStrategy = JsonNamingStrategy.SnakeCase
|
||||
explicitNulls = false
|
||||
coerceInputValues = true
|
||||
}
|
||||
|
||||
// Bean for default jackson object mapper
|
||||
//@Configuration
|
||||
//class JacksonConfig {
|
||||
// @Bean
|
||||
// fun objectMapper(): ObjectMapper {
|
||||
// return JACKSON
|
||||
// }
|
||||
//}
|
||||
@@ -1,22 +0,0 @@
|
||||
package icu.samnyan.aqua;
|
||||
|
||||
import icu.samnyan.aqua.sega.aimedb.AimeDbServer;
|
||||
import icu.samnyan.aqua.spring.util.AutoChecker;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
|
||||
@SpringBootApplication
|
||||
public class AquaServerApplication {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
ConfigurableApplicationContext ctx = SpringApplication.run(AquaServerApplication.class, args);
|
||||
|
||||
final AimeDbServer aimeDbServer = ctx.getBean(AimeDbServer.class);
|
||||
aimeDbServer.start();
|
||||
|
||||
final AutoChecker checker = ctx.getBean(AutoChecker.class);
|
||||
checker.check();
|
||||
}
|
||||
|
||||
}
|
||||
29
src/main/java/icu/samnyan/aqua/Entry.kt
Normal file
29
src/main/java/icu/samnyan/aqua/Entry.kt
Normal file
@@ -0,0 +1,29 @@
|
||||
package icu.samnyan.aqua
|
||||
|
||||
import icu.samnyan.aqua.sega.aimedb.AimeDbServer
|
||||
import icu.samnyan.aqua.spring.AutoChecker
|
||||
import org.springframework.boot.SpringApplication
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.scheduling.annotation.EnableScheduling
|
||||
import java.io.File
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableScheduling
|
||||
class Entry
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
// If data/ is not found, create it
|
||||
File("data").mkdirs()
|
||||
|
||||
// Run the application
|
||||
val ctx = SpringApplication.run(Entry::class.java, *args)
|
||||
|
||||
// Start the AimeDbServer
|
||||
val aimeDbServer = ctx.getBean(AimeDbServer::class.java)
|
||||
aimeDbServer.start()
|
||||
|
||||
// Start the AutoChecker
|
||||
val checker = ctx.getBean(AutoChecker::class.java)
|
||||
checker.check()
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package icu.samnyan.aqua.api.config;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
@@ -10,6 +8,8 @@ import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import org.springframework.web.servlet.resource.PathResourceResolver;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package icu.samnyan.aqua.api.controller.general;
|
||||
|
||||
import icu.samnyan.aqua.sega.diva.dao.userdata.PlayerScreenShotRepository;
|
||||
import icu.samnyan.aqua.sega.diva.model.userdata.PlayerScreenShot;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -19,14 +20,10 @@ import java.util.Optional;
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("api/static")
|
||||
@AllArgsConstructor
|
||||
public class StaticController {
|
||||
|
||||
private final PlayerScreenShotRepository playerScreenShotRepository;
|
||||
|
||||
public StaticController(PlayerScreenShotRepository playerScreenShotRepository) {
|
||||
this.playerScreenShotRepository = playerScreenShotRepository;
|
||||
}
|
||||
|
||||
@GetMapping(value = "screenshot/{filename}", produces = MediaType.IMAGE_JPEG_VALUE)
|
||||
public ResponseEntity<Resource> getScreenshotFile(@PathVariable String filename) {
|
||||
Optional<PlayerScreenShot> ss = playerScreenShotRepository.findByFileName(filename);
|
||||
|
||||
@@ -2,6 +2,7 @@ package icu.samnyan.aqua.api.controller.sega;
|
||||
|
||||
import icu.samnyan.aqua.sega.general.model.Card;
|
||||
import icu.samnyan.aqua.sega.general.service.CardService;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
@@ -16,14 +17,11 @@ import java.util.Optional;
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("api/sega/aime")
|
||||
@AllArgsConstructor
|
||||
public class ApiAimeController {
|
||||
|
||||
private final CardService cardService;
|
||||
|
||||
public ApiAimeController(CardService cardService) {
|
||||
this.cardService = cardService;
|
||||
}
|
||||
|
||||
@PostMapping("getByAccessCode")
|
||||
public Optional<Card> getByAccessCode(@RequestBody Map<String, String> request) {
|
||||
return cardService.getCardByAccessCode(request.get("accessCode").replaceAll("-", "").replaceAll(" ", ""));
|
||||
|
||||
@@ -6,7 +6,10 @@ import icu.samnyan.aqua.sega.chunithm.model.gamedata.Character;
|
||||
import icu.samnyan.aqua.sega.chunithm.model.gamedata.CharacterSkill;
|
||||
import icu.samnyan.aqua.sega.chunithm.model.gamedata.Music;
|
||||
import icu.samnyan.aqua.sega.chunithm.service.GameMusicService;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -15,18 +18,13 @@ import java.util.List;
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("api/game/chuni/v1/data")
|
||||
@AllArgsConstructor
|
||||
public class ApiChuniV1GameDataController {
|
||||
|
||||
private final GameMusicService gameMusicService;
|
||||
private final GameCharacterRepository gameCharacterRepository;
|
||||
private final GameCharacterSkillRepository gameCharacterSkillRepository;
|
||||
|
||||
public ApiChuniV1GameDataController(GameMusicService gameMusicService, GameCharacterRepository gameCharacterRepository, GameCharacterSkillRepository gameCharacterSkillRepository) {
|
||||
this.gameMusicService = gameMusicService;
|
||||
this.gameCharacterRepository = gameCharacterRepository;
|
||||
this.gameCharacterSkillRepository = gameCharacterSkillRepository;
|
||||
}
|
||||
|
||||
@GetMapping("music")
|
||||
public List<Music> getMusic() {
|
||||
return gameMusicService.getAll();
|
||||
@@ -41,15 +39,4 @@ public class ApiChuniV1GameDataController {
|
||||
public List<CharacterSkill> getSkill() {
|
||||
return gameCharacterSkillRepository.findAll();
|
||||
}
|
||||
|
||||
// @PostMapping("character")
|
||||
// public List<Character> importCharacter(@RequestBody List<Character> req) {
|
||||
// return gameCharacterRepository.saveAll(req);
|
||||
// }
|
||||
//
|
||||
// @PostMapping("skill")
|
||||
// public List<CharacterSkill> importSkill(@RequestBody List<CharacterSkill> req) {
|
||||
// return gameCharacterSkillRepository.saveAll(req);
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import icu.samnyan.aqua.sega.general.model.Card;
|
||||
import icu.samnyan.aqua.sega.general.service.CardService;
|
||||
import icu.samnyan.aqua.sega.util.VersionInfo;
|
||||
import icu.samnyan.aqua.sega.util.VersionUtil;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -39,6 +40,7 @@ import java.util.stream.Collectors;
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("api/game/chuni/v1")
|
||||
@AllArgsConstructor
|
||||
public class ApiChuniV1PlayerDataController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ApiChuniV1PlayerDataController.class);
|
||||
@@ -64,28 +66,6 @@ public class ApiChuniV1PlayerDataController {
|
||||
|
||||
private final GameMusicService gameMusicService;
|
||||
|
||||
@Autowired
|
||||
public ApiChuniV1PlayerDataController(ApiMapper mapper, CardService cardService, UserActivityService userActivityService, UserCharacterService userCharacterService, UserChargeService userChargeService, UserDataService userDataService, UserDataExService userDataExService, UserGameOptionExService userGameOptionExService, UserMapService userMapService, UserPlaylogService userPlaylogService, UserMusicDetailService userMusicDetailService, UserCourseService userCourseService, UserDuelService userDuelService, UserGameOptionService userGameOptionService, UserItemService userItemService, UserGeneralDataService userGeneralDataService, GameMusicService gameMusicService) {
|
||||
this.mapper = mapper;
|
||||
this.cardService = cardService;
|
||||
this.userActivityService = userActivityService;
|
||||
this.userCharacterService = userCharacterService;
|
||||
this.userChargeService = userChargeService;
|
||||
this.userDataService = userDataService;
|
||||
this.userDataExService = userDataExService;
|
||||
this.userGameOptionExService = userGameOptionExService;
|
||||
this.userMapService = userMapService;
|
||||
this.userPlaylogService = userPlaylogService;
|
||||
this.userMusicDetailService = userMusicDetailService;
|
||||
this.userCourseService = userCourseService;
|
||||
this.userDuelService = userDuelService;
|
||||
this.userGameOptionService = userGameOptionService;
|
||||
this.userItemService = userItemService;
|
||||
this.userGeneralDataService = userGeneralDataService;
|
||||
this.gameMusicService = gameMusicService;
|
||||
}
|
||||
|
||||
|
||||
// Keep it here for legacy
|
||||
@GetMapping("music")
|
||||
public List<Music> getMusicList() {
|
||||
|
||||
@@ -1,22 +1,12 @@
|
||||
package icu.samnyan.aqua.api.controller.sega.game.chuni.v2;
|
||||
import icu.samnyan.aqua.sega.chusan.dao.gamedata.GameAvatarAccRepository;
|
||||
import icu.samnyan.aqua.sega.chusan.dao.gamedata.GameCharacterRepository;
|
||||
import icu.samnyan.aqua.sega.chusan.dao.gamedata.GameFrameRepository;
|
||||
import icu.samnyan.aqua.sega.chusan.dao.gamedata.GameMapIconRepository;
|
||||
import icu.samnyan.aqua.sega.chusan.dao.gamedata.GameMusicRepository;
|
||||
import icu.samnyan.aqua.sega.chusan.dao.gamedata.GameTrophyRepository;
|
||||
import icu.samnyan.aqua.sega.chusan.dao.gamedata.GameNamePlateRepository;
|
||||
import icu.samnyan.aqua.sega.chusan.dao.gamedata.GameSystemVoiceRepository;
|
||||
import icu.samnyan.aqua.sega.chusan.model.gamedata.AvatarAcc;
|
||||
import icu.samnyan.aqua.sega.chusan.model.gamedata.Character;
|
||||
import icu.samnyan.aqua.sega.chusan.model.gamedata.Frame;
|
||||
import icu.samnyan.aqua.sega.chusan.model.gamedata.MapIcon;
|
||||
import icu.samnyan.aqua.sega.chusan.model.gamedata.Music;
|
||||
import icu.samnyan.aqua.sega.chusan.model.gamedata.NamePlate;
|
||||
import icu.samnyan.aqua.sega.chusan.model.gamedata.SystemVoice;
|
||||
import icu.samnyan.aqua.sega.chusan.model.gamedata.Trophy;
|
||||
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import icu.samnyan.aqua.sega.chusan.model.*;
|
||||
import icu.samnyan.aqua.sega.chusan.model.gamedata.Character;
|
||||
import icu.samnyan.aqua.sega.chusan.model.gamedata.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -25,27 +15,17 @@ import java.util.List;
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("api/game/chuni/v2/data")
|
||||
@AllArgsConstructor
|
||||
public class ApiChuniV2GameDataController {
|
||||
|
||||
private final GameMusicRepository gameMusicRepository;
|
||||
private final GameCharacterRepository gameCharacterRepository;
|
||||
private final GameTrophyRepository gameTrophyRepository;
|
||||
private final GameNamePlateRepository gameNamePlateRepository;
|
||||
private final GameSystemVoiceRepository gameSystemVoiceRepository;
|
||||
private final GameMapIconRepository gameMapIconRepository;
|
||||
private final GameFrameRepository gameFrameRepository;
|
||||
private final GameAvatarAccRepository gameAvatarAccRepository;
|
||||
|
||||
public ApiChuniV2GameDataController(GameMusicRepository gameMusicRepository, GameCharacterRepository gameCharacterRepository, GameTrophyRepository gameTrophyRepository, GameNamePlateRepository gameNamePlateRepository, GameSystemVoiceRepository gameSystemVoiceRepository, GameMapIconRepository gameMapIconRepository, GameFrameRepository gameFrameRepository, GameAvatarAccRepository gameAvatarAccRepository) {
|
||||
this.gameMusicRepository = gameMusicRepository;
|
||||
this.gameCharacterRepository = gameCharacterRepository;
|
||||
this.gameTrophyRepository = gameTrophyRepository;
|
||||
this.gameNamePlateRepository = gameNamePlateRepository;
|
||||
this.gameSystemVoiceRepository = gameSystemVoiceRepository;
|
||||
this.gameMapIconRepository = gameMapIconRepository;
|
||||
this.gameFrameRepository = gameFrameRepository;
|
||||
this.gameAvatarAccRepository = gameAvatarAccRepository;
|
||||
}
|
||||
private final Chu3GameMusicRepo gameMusicRepository;
|
||||
private final Chu3GameCharacterRepo gameCharacterRepository;
|
||||
private final Chu3GameTrophyRepo gameTrophyRepository;
|
||||
private final Chu3GameNamePlateRepo gameNamePlateRepository;
|
||||
private final Chu3GameSystemVoiceRepo gameSystemVoiceRepository;
|
||||
private final Chu3GameMapIconRepo gameMapIconRepository;
|
||||
private final Chu3GameFrameRepo gameFrameRepository;
|
||||
private final Chu3GameAvatarAccRepo gameAvatarAccRepository;
|
||||
|
||||
@GetMapping("music")
|
||||
public List<Music> getMusic() {
|
||||
|
||||
@@ -6,11 +6,10 @@ import icu.samnyan.aqua.api.model.ReducedPageResponse;
|
||||
import icu.samnyan.aqua.api.model.resp.sega.chuni.v2.ProfileResp;
|
||||
import icu.samnyan.aqua.api.model.resp.sega.chuni.v2.RatingItem;
|
||||
import icu.samnyan.aqua.api.model.resp.sega.chuni.v2.RecentResp;
|
||||
import icu.samnyan.aqua.api.model.resp.sega.chuni.v2.external.ChuniDataExport;
|
||||
import icu.samnyan.aqua.api.model.resp.sega.chuni.v2.external.Chu3DataExport;
|
||||
import icu.samnyan.aqua.api.model.resp.sega.chuni.v2.external.ChuniDataImport;
|
||||
import icu.samnyan.aqua.api.model.resp.sega.chuni.v2.external.ExternalUserData;
|
||||
import icu.samnyan.aqua.api.util.ApiMapper;
|
||||
import icu.samnyan.aqua.sega.chunithm.handler.impl.GetUserFavoriteMusicHandler;
|
||||
import icu.samnyan.aqua.sega.chusan.model.gamedata.Level;
|
||||
import icu.samnyan.aqua.sega.chusan.model.gamedata.Music;
|
||||
import icu.samnyan.aqua.sega.chusan.model.userdata.*;
|
||||
@@ -19,10 +18,10 @@ import icu.samnyan.aqua.sega.general.model.Card;
|
||||
import icu.samnyan.aqua.sega.general.service.CardService;
|
||||
import icu.samnyan.aqua.sega.util.VersionInfo;
|
||||
import icu.samnyan.aqua.sega.util.VersionUtil;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
@@ -40,6 +39,7 @@ import java.util.stream.Collectors;
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("api/game/chuni/v2")
|
||||
@AllArgsConstructor
|
||||
public class ApiChuniV2PlayerDataController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ApiChuniV2PlayerDataController.class);
|
||||
@@ -62,33 +62,6 @@ public class ApiChuniV2PlayerDataController {
|
||||
private final UserGeneralDataService userGeneralDataService;
|
||||
private final GameMusicService gameMusicService;
|
||||
|
||||
@Autowired
|
||||
public ApiChuniV2PlayerDataController(ApiMapper mapper, CardService cardService, UserActivityService userActivityService, UserCharacterService userCharacterService, UserChargeService userChargeService, UserDataService userDataService, UserMapAreaService userMapAreaService, UserPlaylogService userPlaylogService, UserMusicDetailService userMusicDetailService, UserCourseService userCourseService, UserDuelService userDuelService, UserGameOptionService userGameOptionService, UserItemService userItemService, UserGeneralDataService userGeneralDataService, GameMusicService gameMusicService) {
|
||||
this.mapper = mapper;
|
||||
this.cardService = cardService;
|
||||
this.userActivityService = userActivityService;
|
||||
this.userCharacterService = userCharacterService;
|
||||
this.userChargeService = userChargeService;
|
||||
this.userDataService = userDataService;
|
||||
this.userMapAreaService = userMapAreaService;
|
||||
this.userPlaylogService = userPlaylogService;
|
||||
this.userMusicDetailService = userMusicDetailService;
|
||||
this.userCourseService = userCourseService;
|
||||
this.userDuelService = userDuelService;
|
||||
this.userGameOptionService = userGameOptionService;
|
||||
this.userItemService = userItemService;
|
||||
this.userGeneralDataService = userGeneralDataService;
|
||||
this.gameMusicService = gameMusicService;
|
||||
}
|
||||
|
||||
/*
|
||||
// Keep it here for legacy
|
||||
@GetMapping("music")
|
||||
public List<Music> getMusicList() {
|
||||
return gameMusicService.getAll();
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get Basic info
|
||||
*
|
||||
@@ -108,64 +81,64 @@ public class ApiChuniV2PlayerDataController {
|
||||
}
|
||||
|
||||
@PutMapping("profile/username")
|
||||
public UserData updateName(@RequestBody Map<String, Object> request) {
|
||||
UserData profile = userDataService.getUserByExtId((String) request.get("aimeId")).orElseThrow();
|
||||
public Chu3UserData updateName(@RequestBody Map<String, Object> request) {
|
||||
Chu3UserData profile = userDataService.getUserByExtId((String) request.get("aimeId")).orElseThrow();
|
||||
profile.setUserName((String) request.get("userName"));
|
||||
return userDataService.saveUserData(profile);
|
||||
}
|
||||
|
||||
@PutMapping("profile/romversion")
|
||||
public UserData updateRomVersion(@RequestBody Map<String, Object> request) {
|
||||
UserData profile = userDataService.getUserByExtId((String) request.get("aimeId")).orElseThrow();
|
||||
public Chu3UserData updateRomVersion(@RequestBody Map<String, Object> request) {
|
||||
Chu3UserData profile = userDataService.getUserByExtId((String) request.get("aimeId")).orElseThrow();
|
||||
profile.setLastRomVersion((String) request.get("romVersion"));
|
||||
return userDataService.saveUserData(profile);
|
||||
}
|
||||
|
||||
@PutMapping("profile/dataversion")
|
||||
public UserData updateDataVersion(@RequestBody Map<String, Object> request) {
|
||||
UserData profile = userDataService.getUserByExtId((String) request.get("aimeId")).orElseThrow();
|
||||
public Chu3UserData updateDataVersion(@RequestBody Map<String, Object> request) {
|
||||
Chu3UserData profile = userDataService.getUserByExtId((String) request.get("aimeId")).orElseThrow();
|
||||
profile.setLastDataVersion((String) request.get("dataVersion"));
|
||||
return userDataService.saveUserData(profile);
|
||||
}
|
||||
|
||||
@PutMapping("profile/plate")
|
||||
public UserData updatePlate(@RequestBody Map<String, Object> request) {
|
||||
UserData profile = userDataService.getUserByExtId((String) request.get("aimeId")).orElseThrow();
|
||||
public Chu3UserData updatePlate(@RequestBody Map<String, Object> request) {
|
||||
Chu3UserData profile = userDataService.getUserByExtId((String) request.get("aimeId")).orElseThrow();
|
||||
profile.setNameplateId((Integer) request.get("nameplateId"));
|
||||
return userDataService.saveUserData(profile);
|
||||
}
|
||||
|
||||
@PutMapping("profile/frame")
|
||||
public UserData updateFrame(@RequestBody Map<String, Object> request) {
|
||||
UserData profile = userDataService.getUserByExtId((String) request.get("aimeId")).orElseThrow();
|
||||
public Chu3UserData updateFrame(@RequestBody Map<String, Object> request) {
|
||||
Chu3UserData profile = userDataService.getUserByExtId((String) request.get("aimeId")).orElseThrow();
|
||||
profile.setFrameId((Integer) request.get("frameId"));
|
||||
return userDataService.saveUserData(profile);
|
||||
}
|
||||
|
||||
@PutMapping("profile/trophy")
|
||||
public UserData updateTrophy(@RequestBody Map<String, Object> request) {
|
||||
UserData profile = userDataService.getUserByExtId((String) request.get("aimeId")).orElseThrow();
|
||||
public Chu3UserData updateTrophy(@RequestBody Map<String, Object> request) {
|
||||
Chu3UserData profile = userDataService.getUserByExtId((String) request.get("aimeId")).orElseThrow();
|
||||
profile.setTrophyId((Integer) request.get("trophyId"));
|
||||
return userDataService.saveUserData(profile);
|
||||
}
|
||||
|
||||
@PutMapping("profile/mapicon")
|
||||
public UserData updateMapIcon(@RequestBody Map<String, Object> request) {
|
||||
UserData profile = userDataService.getUserByExtId((String) request.get("aimeId")).orElseThrow();
|
||||
public Chu3UserData updateMapIcon(@RequestBody Map<String, Object> request) {
|
||||
Chu3UserData profile = userDataService.getUserByExtId((String) request.get("aimeId")).orElseThrow();
|
||||
profile.setMapIconId((Integer) request.get("mapiconId"));
|
||||
return userDataService.saveUserData(profile);
|
||||
}
|
||||
|
||||
@PutMapping("profile/sysvoice")
|
||||
public UserData updateSystemVoice(@RequestBody Map<String, Object> request) {
|
||||
UserData profile = userDataService.getUserByExtId((String) request.get("aimeId")).orElseThrow();
|
||||
public Chu3UserData updateSystemVoice(@RequestBody Map<String, Object> request) {
|
||||
Chu3UserData profile = userDataService.getUserByExtId((String) request.get("aimeId")).orElseThrow();
|
||||
profile.setVoiceId((Integer) request.get("voiceId"));
|
||||
return userDataService.saveUserData(profile);
|
||||
}
|
||||
|
||||
@PutMapping("profile/avatar")
|
||||
public UserData updateAvatar(@RequestBody Map<String, Object> request) {
|
||||
UserData profile = userDataService.getUserByExtId((String) request.get("aimeId")).orElseThrow();
|
||||
public Chu3UserData updateAvatar(@RequestBody Map<String, Object> request) {
|
||||
Chu3UserData profile = userDataService.getUserByExtId((String) request.get("aimeId")).orElseThrow();
|
||||
int category = (Integer) request.get("category");
|
||||
switch (category) {
|
||||
case 1:
|
||||
@@ -196,7 +169,7 @@ public class ApiChuniV2PlayerDataController {
|
||||
|
||||
@PutMapping("profile/privacy")
|
||||
public ResponseEntity<Object> updatePrivacy(@RequestBody Map<String, Object> request) {
|
||||
UserData profile = userDataService.getUserByExtId((String) request.get("aimeId")).orElseThrow();
|
||||
Chu3UserData profile = userDataService.getUserByExtId((String) request.get("aimeId")).orElseThrow();
|
||||
UserGameOption option = userGameOptionService.getByUser(profile).orElseThrow();
|
||||
int privacy = (Integer) request.get("privacy");
|
||||
if (privacy != 1 && privacy != 0) {
|
||||
@@ -327,7 +300,7 @@ public class ApiChuniV2PlayerDataController {
|
||||
|
||||
@PutMapping("song/{id}/favorite")
|
||||
public void updateSongFavorite(@RequestParam String aimeId, @PathVariable String id) {
|
||||
UserData profile = userDataService.getUserByExtId(aimeId).orElseThrow();
|
||||
Chu3UserData profile = userDataService.getUserByExtId(aimeId).orElseThrow();
|
||||
UserGeneralData userGeneralData = userGeneralDataService.getByUserAndKey(profile, "favorite_music")
|
||||
.orElseGet(() -> new UserGeneralData(profile, "favorite_music"));
|
||||
List<String> favoriteSongs = new LinkedList<String>(Arrays.asList(userGeneralData.getPropertyValue().split(",")));
|
||||
@@ -356,7 +329,7 @@ public class ApiChuniV2PlayerDataController {
|
||||
|
||||
@PostMapping("character")
|
||||
public ResponseEntity<Object> updateCharacter(@RequestBody Map<String, Object> request) {
|
||||
UserData profile = userDataService.getUserByExtId((String) request.get("aimeId")).orElseThrow();
|
||||
Chu3UserData profile = userDataService.getUserByExtId((String) request.get("aimeId")).orElseThrow();
|
||||
Integer characterId = (Integer) request.get("characterId");
|
||||
Optional<UserCharacter> characterOptional = userCharacterService.getByUserAndCharacterId(profile, characterId);
|
||||
UserCharacter character;
|
||||
@@ -398,7 +371,7 @@ public class ApiChuniV2PlayerDataController {
|
||||
|
||||
@PostMapping("item")
|
||||
public ResponseEntity<Object> updateItem(@RequestBody Map<String, Object> request) {
|
||||
UserData profile = userDataService.getUserByExtId((String) request.get("aimeId")).orElseThrow();
|
||||
Chu3UserData profile = userDataService.getUserByExtId((String) request.get("aimeId")).orElseThrow();
|
||||
Integer itemId = (Integer) request.get("itemId");
|
||||
Integer itemKind = (Integer) request.get("itemKind");
|
||||
Optional<UserItem> itemOptional = userItemService.getByUserAndItemIdAndKind(profile, itemId,itemKind);
|
||||
@@ -425,7 +398,7 @@ public class ApiChuniV2PlayerDataController {
|
||||
|
||||
@GetMapping("export")
|
||||
public ResponseEntity<Object> exportAllUserData(@RequestParam String aimeId) {
|
||||
ChuniDataExport data = new ChuniDataExport();
|
||||
Chu3DataExport data = new Chu3DataExport();
|
||||
try {
|
||||
data.setGameId("SDHD");
|
||||
data.setUserData(userDataService.getUserByExtId(aimeId).orElseThrow());
|
||||
@@ -473,7 +446,7 @@ public class ApiChuniV2PlayerDataController {
|
||||
card = cardService.registerByAccessCode(exUser.getAccessCode());
|
||||
}
|
||||
|
||||
UserData userData = mapper.convert(exUser, new TypeReference<>() {
|
||||
Chu3UserData userData = mapper.convert(exUser, new TypeReference<>() {
|
||||
});
|
||||
userData.setCard(card);
|
||||
userDataService.saveAndFlushUserData(userData);
|
||||
@@ -506,7 +479,7 @@ public class ApiChuniV2PlayerDataController {
|
||||
userItemList.forEach(x -> x.setUser(userData));
|
||||
userItemService.saveAll(userItemList);
|
||||
|
||||
List<UserMapArea> userMapList = data.getUserMapList();
|
||||
List<UserMap> userMapList = data.getUserMapList();
|
||||
userMapList.forEach(x -> x.setUser(userData));
|
||||
userMapAreaService.saveAll(userMapList);
|
||||
|
||||
|
||||
@@ -6,15 +6,11 @@ import icu.samnyan.aqua.sega.diva.dao.gamedata.DivaPvRepository;
|
||||
import icu.samnyan.aqua.sega.diva.model.gamedata.DivaCustomize;
|
||||
import icu.samnyan.aqua.sega.diva.model.gamedata.DivaModule;
|
||||
import icu.samnyan.aqua.sega.diva.model.gamedata.Pv;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.MediaType;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -22,18 +18,13 @@ import java.util.List;
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("api/game/diva/data")
|
||||
@AllArgsConstructor
|
||||
public class ApiDivaGameDataController {
|
||||
|
||||
private final DivaModuleRepository divaModuleRepository;
|
||||
private final DivaCustomizeRepository divaCustomizeRepository;
|
||||
private final DivaPvRepository divaPvRepository;
|
||||
|
||||
public ApiDivaGameDataController(DivaModuleRepository divaModuleRepository, DivaCustomizeRepository divaCustomizeRepository, DivaPvRepository divaPvRepository) {
|
||||
this.divaModuleRepository = divaModuleRepository;
|
||||
this.divaCustomizeRepository = divaCustomizeRepository;
|
||||
this.divaPvRepository = divaPvRepository;
|
||||
}
|
||||
|
||||
@GetMapping(value = "musicList")
|
||||
public List<Pv> musicList() {
|
||||
return divaPvRepository.findAll();
|
||||
|
||||
@@ -8,6 +8,7 @@ import icu.samnyan.aqua.sega.diva.model.common.Difficulty;
|
||||
import icu.samnyan.aqua.sega.diva.model.common.Edition;
|
||||
import icu.samnyan.aqua.sega.diva.model.userdata.*;
|
||||
import icu.samnyan.aqua.sega.diva.service.PlayerProfileService;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@@ -21,6 +22,7 @@ import java.util.*;
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("api/game/diva")
|
||||
@AllArgsConstructor
|
||||
public class ApiDivaPlayerDataController {
|
||||
|
||||
private final PlayerProfileService playerProfileService;
|
||||
@@ -33,19 +35,8 @@ public class ApiDivaPlayerDataController {
|
||||
private final PlayerCustomizeRepository playerCustomizeRepository;
|
||||
private final PlayerScreenShotRepository playerScreenShotRepository;
|
||||
|
||||
public ApiDivaPlayerDataController(PlayerProfileService playerProfileService, GameSessionRepository gameSessionRepository, PlayLogRepository playLogRepository, PlayerPvRecordRepository playerPvRecordRepository, PlayerPvCustomizeRepository playerPvCustomizeRepository, PlayerModuleRepository playerModuleRepository, PlayerCustomizeRepository playerCustomizeRepository, PlayerScreenShotRepository playerScreenShotRepository) {
|
||||
this.playerProfileService = playerProfileService;
|
||||
this.gameSessionRepository = gameSessionRepository;
|
||||
this.playLogRepository = playLogRepository;
|
||||
this.playerPvRecordRepository = playerPvRecordRepository;
|
||||
this.playerPvCustomizeRepository = playerPvCustomizeRepository;
|
||||
this.playerModuleRepository = playerModuleRepository;
|
||||
this.playerCustomizeRepository = playerCustomizeRepository;
|
||||
this.playerScreenShotRepository = playerScreenShotRepository;
|
||||
}
|
||||
|
||||
@PostMapping("forceUnlock")
|
||||
public ResponseEntity<MessageResponse> forceUnlock(@RequestParam int pdId) {
|
||||
public ResponseEntity<MessageResponse> forceUnlock(@RequestParam long pdId) {
|
||||
PlayerProfile profile = playerProfileService.findByPdId(pdId).orElseThrow();
|
||||
Optional<GameSession> session = gameSessionRepository.findByPdId(profile);
|
||||
if(session.isPresent()) {
|
||||
@@ -57,13 +48,13 @@ public class ApiDivaPlayerDataController {
|
||||
}
|
||||
|
||||
@GetMapping("playerInfo")
|
||||
public Optional<PlayerProfile> getPlayerInfo(@RequestParam int pdId) {
|
||||
public Optional<PlayerProfile> getPlayerInfo(@RequestParam long pdId) {
|
||||
return playerProfileService.findByPdId(pdId);
|
||||
}
|
||||
|
||||
@GetMapping("playerInfo/rival")
|
||||
public Map<String, String> getRivalInfo(@RequestParam int pdId) {
|
||||
int rId = playerProfileService.findByPdId(pdId).orElseThrow().getRivalPdId();
|
||||
public Map<String, String> getRivalInfo(@RequestParam long pdId) {
|
||||
var rId = playerProfileService.findByPdId(pdId).orElseThrow().getRivalPdId();
|
||||
Map<String, String> result = new HashMap<>();
|
||||
if (rId == -1) {
|
||||
result.put("rival", "Not Set");
|
||||
@@ -176,7 +167,7 @@ public class ApiDivaPlayerDataController {
|
||||
}
|
||||
|
||||
@GetMapping("playLog")
|
||||
public ReducedPageResponse<PlayLog> getPlayLogs(@RequestParam int pdId,
|
||||
public ReducedPageResponse<PlayLog> getPlayLogs(@RequestParam long pdId,
|
||||
@RequestParam(required = false, defaultValue = "0") int page,
|
||||
@RequestParam(required = false, defaultValue = "10") int size) {
|
||||
Page<PlayLog> playLogs = playLogRepository.findByPdId_PdIdOrderByDateTimeDesc(pdId, PageRequest.of(page, size));
|
||||
@@ -188,7 +179,7 @@ public class ApiDivaPlayerDataController {
|
||||
*/
|
||||
|
||||
@GetMapping("pvRecord")
|
||||
public ReducedPageResponse<PlayerPvRecord> getPvRecords(@RequestParam int pdId,
|
||||
public ReducedPageResponse<PlayerPvRecord> getPvRecords(@RequestParam long pdId,
|
||||
@RequestParam(required = false, defaultValue = "0") int page,
|
||||
@RequestParam(required = false, defaultValue = "10") int size) {
|
||||
Page<PlayerPvRecord> pvRecords = playerPvRecordRepository.findByPdId_PdIdOrderByPvId(pdId, PageRequest.of(page, size));
|
||||
@@ -196,7 +187,7 @@ public class ApiDivaPlayerDataController {
|
||||
}
|
||||
|
||||
@GetMapping("pvRecord/{pvId}")
|
||||
public Map<String, Object> getPvRecord(@RequestParam int pdId, @PathVariable int pvId) {
|
||||
public Map<String, Object> getPvRecord(@RequestParam long pdId, @PathVariable int pvId) {
|
||||
Map<String, Object> resultMap = new HashMap<>();
|
||||
resultMap.put("records", playerPvRecordRepository.findByPdId_PdIdAndPvId(pdId, pvId));
|
||||
playerPvCustomizeRepository.findByPdId_PdIdAndPvId(pdId, pvId).ifPresent(x -> resultMap.put("customize", x));
|
||||
@@ -260,7 +251,7 @@ public class ApiDivaPlayerDataController {
|
||||
}
|
||||
|
||||
@GetMapping("module")
|
||||
public ReducedPageResponse<PlayerModule> getModules(@RequestParam int pdId,
|
||||
public ReducedPageResponse<PlayerModule> getModules(@RequestParam long pdId,
|
||||
@RequestParam(required = false, defaultValue = "0") int page,
|
||||
@RequestParam(required = false, defaultValue = "10") int size) {
|
||||
Page<PlayerModule> modules = playerModuleRepository.findByPdId_PdId(pdId, PageRequest.of(page, size));
|
||||
@@ -268,7 +259,7 @@ public class ApiDivaPlayerDataController {
|
||||
}
|
||||
|
||||
@GetMapping("customize")
|
||||
public ReducedPageResponse<PlayerCustomize> getCustomizes(@RequestParam int pdId,
|
||||
public ReducedPageResponse<PlayerCustomize> getCustomizes(@RequestParam long pdId,
|
||||
@RequestParam(required = false, defaultValue = "0") int page,
|
||||
@RequestParam(required = false, defaultValue = "10") int size) {
|
||||
Page<PlayerCustomize> customizes = playerCustomizeRepository.findByPdId_PdId(pdId, PageRequest.of(page, size));
|
||||
@@ -276,7 +267,7 @@ public class ApiDivaPlayerDataController {
|
||||
}
|
||||
|
||||
@GetMapping("screenshot")
|
||||
public List<PlayerScreenShot> getScreenshotList(@RequestParam int pdId) {
|
||||
public List<PlayerScreenShot> getScreenshotList(@RequestParam long pdId) {
|
||||
return playerScreenShotRepository.findByPdId_PdId(pdId);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,19 +2,19 @@ package icu.samnyan.aqua.api.controller.sega.game.maimai2;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import icu.samnyan.aqua.api.model.MessageResponse;
|
||||
import icu.samnyan.aqua.api.model.ReducedPageResponse;
|
||||
import icu.samnyan.aqua.api.model.resp.sega.maimai2.ProfileResp;
|
||||
import icu.samnyan.aqua.api.model.resp.sega.maimai2.PhotoResp;
|
||||
import icu.samnyan.aqua.api.model.resp.sega.maimai2.ProfileResp;
|
||||
import icu.samnyan.aqua.api.model.resp.sega.maimai2.external.ExternalUserData;
|
||||
import icu.samnyan.aqua.api.model.resp.sega.maimai2.external.Maimai2DataExport;
|
||||
import icu.samnyan.aqua.api.model.resp.sega.maimai2.external.Maimai2DataImport;
|
||||
import icu.samnyan.aqua.api.util.ApiMapper;
|
||||
import icu.samnyan.aqua.sega.general.model.Card;
|
||||
import icu.samnyan.aqua.sega.general.service.CardService;
|
||||
import icu.samnyan.aqua.sega.maimai2.dao.userdata.*;
|
||||
import icu.samnyan.aqua.sega.maimai2.model.*;
|
||||
import icu.samnyan.aqua.sega.maimai2.model.userdata.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
@@ -24,9 +24,9 @@ import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.*;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
@@ -36,59 +36,30 @@ import java.util.stream.Stream;
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("api/game/maimai2")
|
||||
@AllArgsConstructor
|
||||
public class ApiMaimai2PlayerDataController {
|
||||
|
||||
private final ApiMapper mapper;
|
||||
|
||||
private static DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.0");
|
||||
|
||||
private final CardService cardService;
|
||||
|
||||
private final UserActRepository userActRepository;
|
||||
private final UserCharacterRepository userCharacterRepository;
|
||||
private final UserDataRepository userDataRepository;
|
||||
private final UserItemRepository userItemRepository;
|
||||
private final UserLoginBonusRepository userLoginBonusRepository;
|
||||
private final UserMusicDetailRepository userMusicDetailRepository;
|
||||
private final UserOptionRepository userOptionRepository;
|
||||
private final UserPlaylogRepository userPlaylogRepository;
|
||||
private final UserGeneralDataRepository userGeneralDataRepository;
|
||||
private final MapEncountNpcRepository mapEncountNpcRepository;
|
||||
private final UserChargeRepository userChargeRepository;
|
||||
private final UserCourseRepository userCourseRepository;
|
||||
private final UserExtendRepository userExtendRepository;
|
||||
private final UserFavoriteRepository userFavoriteRepository;
|
||||
private final UserFriendSeasonRankingRepository userFriendSeasonRankingRepository;
|
||||
private final UserMapRepository userMapRepository;
|
||||
private final UserUdemaeRepository userUdemaeRepository;
|
||||
|
||||
public ApiMaimai2PlayerDataController(ApiMapper mapper, CardService cardService, UserActRepository userActRepository,
|
||||
UserCharacterRepository userCharacterRepository, UserDataRepository userDataRepository, UserItemRepository userItemRepository,
|
||||
UserLoginBonusRepository userLoginBonusRepository, UserMusicDetailRepository userMusicDetailRepository, UserOptionRepository userOptionRepository,
|
||||
UserPlaylogRepository userPlaylogRepository, UserGeneralDataRepository userGeneralDataRepository, MapEncountNpcRepository mapEncountNpcRepository,
|
||||
UserChargeRepository userChargeRepository, UserCourseRepository userCourseRepository, UserExtendRepository userExtendRepository,
|
||||
UserFavoriteRepository userFavoriteRepository, UserFriendSeasonRankingRepository userFriendSeasonRankingRepository, UserMapRepository userMapRepository,
|
||||
UserUdemaeRepository userUdemaeRepository) {
|
||||
this.mapper = mapper;
|
||||
this.cardService = cardService;
|
||||
this.userActRepository = userActRepository;
|
||||
this.userCharacterRepository = userCharacterRepository;
|
||||
this.userDataRepository = userDataRepository;
|
||||
this.userItemRepository = userItemRepository;
|
||||
this.userLoginBonusRepository = userLoginBonusRepository;
|
||||
this.userMusicDetailRepository = userMusicDetailRepository;
|
||||
this.userOptionRepository = userOptionRepository;
|
||||
this.userPlaylogRepository = userPlaylogRepository;
|
||||
this.userGeneralDataRepository = userGeneralDataRepository;
|
||||
this.mapEncountNpcRepository = mapEncountNpcRepository;
|
||||
this.userChargeRepository = userChargeRepository;
|
||||
this.userCourseRepository = userCourseRepository;
|
||||
this.userExtendRepository = userExtendRepository;
|
||||
this.userFavoriteRepository = userFavoriteRepository;
|
||||
this.userFriendSeasonRankingRepository = userFriendSeasonRankingRepository;
|
||||
this.userMapRepository = userMapRepository;
|
||||
this.userUdemaeRepository = userUdemaeRepository;
|
||||
}
|
||||
private final Mai2UserActRepo userActRepository;
|
||||
private final Mai2UserCharacterRepo userCharacterRepository;
|
||||
private final Mai2UserDataRepo userDataRepository;
|
||||
private final Mai2UserItemRepo userItemRepository;
|
||||
private final Mai2UserLoginBonusRepo userLoginBonusRepository;
|
||||
private final Mai2UserMusicDetailRepo userMusicDetailRepository;
|
||||
private final Mai2UserOptionRepo userOptionRepository;
|
||||
private final Mai2UserPlaylogRepo userPlaylogRepository;
|
||||
private final Mai2UserGeneralDataRepo userGeneralDataRepository;
|
||||
private final Mai2MapEncountNpcRepo mapEncountNpcRepository;
|
||||
private final Mai2UserChargeRepo userChargeRepository;
|
||||
private final Mai2UserCourseRepo userCourseRepository;
|
||||
private final Mai2UserExtendRepo userExtendRepository;
|
||||
private final Mai2UserFavoriteRepo userFavoriteRepository;
|
||||
private final Mai2UserFriendSeasonRankingRepo userFriendSeasonRankingRepository;
|
||||
private final Mai2UserMapRepo userMapRepository;
|
||||
private final Mai2UserUdemaeRepo userUdemaeRepository;
|
||||
|
||||
@GetMapping("config/userPhoto/divMaxLength")
|
||||
public long getConfigUserPhotoDivMaxLength(@Value("${game.maimai2.userPhoto.divMaxLength:32}") long divMaxLength) {
|
||||
@@ -112,10 +83,10 @@ public class ApiMaimai2PlayerDataController {
|
||||
.map(Path::getFileName)
|
||||
.map(Path::toString)
|
||||
.sorted(Comparator.reverseOrder())
|
||||
.collect(Collectors.toList());
|
||||
.toList();
|
||||
Photo.setTotalImage(matchedFiles.size());
|
||||
Photo.setImageIndex(imageIndex);
|
||||
if(matchedFiles.size() > imageIndex){
|
||||
if(matchedFiles.size() > imageIndex) {
|
||||
byte[] targetImageContent = Files.readAllBytes(Paths.get("data/" + matchedFiles.get(imageIndex)));
|
||||
String divData = Base64.getEncoder().encodeToString(targetImageContent);
|
||||
Photo.setDivData(divData);
|
||||
@@ -129,71 +100,71 @@ public class ApiMaimai2PlayerDataController {
|
||||
|
||||
@GetMapping("profile")
|
||||
public ProfileResp getProfile(@RequestParam long aimeId) {
|
||||
return mapper.convert(userDataRepository.findByCard_ExtId(aimeId).orElseThrow(), new TypeReference<>() {
|
||||
return mapper.convert(userDataRepository.findByCardExtId(aimeId).orElseThrow(), new TypeReference<>() {
|
||||
});
|
||||
}
|
||||
|
||||
@PostMapping("profile/username")
|
||||
public UserDetail updateName(@RequestBody Map<String, Object> request) {
|
||||
UserDetail profile = userDataRepository.findByCard_ExtId(((Number) request.get("aimeId")).longValue()).orElseThrow();
|
||||
public Mai2UserDetail updateName(@RequestBody Map<String, Object> request) {
|
||||
Mai2UserDetail profile = userDataRepository.findByCardExtId(((Number) request.get("aimeId")).longValue()).orElseThrow();
|
||||
profile.setUserName((String) request.get("userName"));
|
||||
return userDataRepository.save(profile);
|
||||
}
|
||||
|
||||
@PostMapping("profile/icon")
|
||||
public UserDetail updateIcon(@RequestBody Map<String, Object> request) {
|
||||
UserDetail profile = userDataRepository.findByCard_ExtId(((Number) request.get("aimeId")).longValue()).orElseThrow();
|
||||
public Mai2UserDetail updateIcon(@RequestBody Map<String, Object> request) {
|
||||
Mai2UserDetail profile = userDataRepository.findByCardExtId(((Number) request.get("aimeId")).longValue()).orElseThrow();
|
||||
profile.setIconId((Integer) request.get("iconId"));
|
||||
return userDataRepository.save(profile);
|
||||
}
|
||||
|
||||
@PostMapping("profile/plate")
|
||||
public UserDetail updatePlate(@RequestBody Map<String, Object> request) {
|
||||
UserDetail profile = userDataRepository.findByCard_ExtId(((Number) request.get("aimeId")).longValue()).orElseThrow();
|
||||
public Mai2UserDetail updatePlate(@RequestBody Map<String, Object> request) {
|
||||
Mai2UserDetail profile = userDataRepository.findByCardExtId(((Number) request.get("aimeId")).longValue()).orElseThrow();
|
||||
profile.setPlateId((Integer) request.get("plateId"));
|
||||
return userDataRepository.save(profile);
|
||||
}
|
||||
|
||||
@PostMapping("profile/frame")
|
||||
public UserDetail updateFrame(@RequestBody Map<String, Object> request) {
|
||||
UserDetail profile = userDataRepository.findByCard_ExtId(((Number) request.get("aimeId")).longValue()).orElseThrow();
|
||||
public Mai2UserDetail updateFrame(@RequestBody Map<String, Object> request) {
|
||||
Mai2UserDetail profile = userDataRepository.findByCardExtId(((Number) request.get("aimeId")).longValue()).orElseThrow();
|
||||
profile.setFrameId((Integer) request.get("frameId"));
|
||||
return userDataRepository.save(profile);
|
||||
}
|
||||
|
||||
@PostMapping("profile/title")
|
||||
public UserDetail updateTrophy(@RequestBody Map<String, Object> request) {
|
||||
UserDetail profile = userDataRepository.findByCard_ExtId(((Number) request.get("aimeId")).longValue()).orElseThrow();
|
||||
public Mai2UserDetail updateTrophy(@RequestBody Map<String, Object> request) {
|
||||
Mai2UserDetail profile = userDataRepository.findByCardExtId(((Number) request.get("aimeId")).longValue()).orElseThrow();
|
||||
profile.setTitleId((Integer) request.get("titleId"));
|
||||
return userDataRepository.save(profile);
|
||||
}
|
||||
|
||||
@PostMapping("profile/partner")
|
||||
public UserDetail updatePartner(@RequestBody Map<String, Object> request) {
|
||||
UserDetail profile = userDataRepository.findByCard_ExtId(((Number) request.get("aimeId")).longValue()).orElseThrow();
|
||||
public Mai2UserDetail updatePartner(@RequestBody Map<String, Object> request) {
|
||||
Mai2UserDetail profile = userDataRepository.findByCardExtId(((Number) request.get("aimeId")).longValue()).orElseThrow();
|
||||
profile.setPartnerId((Integer) request.get("partnerId"));
|
||||
return userDataRepository.save(profile);
|
||||
}
|
||||
|
||||
@GetMapping("character")
|
||||
public ReducedPageResponse<UserCharacter> getCharacter(@RequestParam long aimeId,
|
||||
@RequestParam(required = false, defaultValue = "0") int page,
|
||||
@RequestParam(required = false, defaultValue = "10") int size) {
|
||||
Page<UserCharacter> characters = userCharacterRepository.findByUser_Card_ExtId(aimeId, PageRequest.of(page, size));
|
||||
public ReducedPageResponse<Mai2UserCharacter> getCharacter(@RequestParam long aimeId,
|
||||
@RequestParam(required = false, defaultValue = "0") int page,
|
||||
@RequestParam(required = false, defaultValue = "10") int size) {
|
||||
Page<Mai2UserCharacter> characters = userCharacterRepository.findByUser_Card_ExtId(aimeId, PageRequest.of(page, size));
|
||||
return new ReducedPageResponse<>(characters.getContent(), characters.getPageable().getPageNumber(), characters.getTotalPages(), characters.getTotalElements());
|
||||
}
|
||||
|
||||
@GetMapping("activity")
|
||||
public List<UserAct> getActivities(@RequestParam long aimeId) {
|
||||
public List<Mai2UserAct> getActivities(@RequestParam long aimeId) {
|
||||
return userActRepository.findByUser_Card_ExtId(aimeId);
|
||||
}
|
||||
|
||||
@GetMapping("item")
|
||||
public ReducedPageResponse<UserItem> getItem(@RequestParam long aimeId,
|
||||
@RequestParam(required = false, defaultValue = "0") int page,
|
||||
@RequestParam(required = false, defaultValue = "10") int size,
|
||||
@RequestParam(required = false, defaultValue = "0") int ItemKind) {
|
||||
Page<UserItem> items;
|
||||
public ReducedPageResponse<Mai2UserItem> getItem(@RequestParam long aimeId,
|
||||
@RequestParam(required = false, defaultValue = "0") int page,
|
||||
@RequestParam(required = false, defaultValue = "10") int size,
|
||||
@RequestParam(required = false, defaultValue = "0") int ItemKind) {
|
||||
Page<Mai2UserItem> items;
|
||||
if(ItemKind == 0){
|
||||
items = userItemRepository.findByUser_Card_ExtId(aimeId, PageRequest.of(page, size));
|
||||
}
|
||||
@@ -205,7 +176,7 @@ public class ApiMaimai2PlayerDataController {
|
||||
|
||||
@PostMapping("item")
|
||||
public ResponseEntity<Object> updateItem(@RequestBody Map<String, Object> request) {
|
||||
UserDetail profile = userDataRepository.findByCard_ExtId(((Number) request.get("aimeId")).longValue()).orElseThrow();
|
||||
Mai2UserDetail profile = userDataRepository.findByCardExtId(((Number) request.get("aimeId")).longValue()).orElseThrow();
|
||||
Integer itemKind = (Integer) request.get("itemKind");
|
||||
Integer itemId = (Integer) request.get("itemId");
|
||||
int stock = 1;
|
||||
@@ -213,13 +184,14 @@ public class ApiMaimai2PlayerDataController {
|
||||
stock = (Integer) request.get("stock");
|
||||
}
|
||||
|
||||
Optional<UserItem> userItemOptional = userItemRepository.findByUserAndItemKindAndItemId(profile, itemKind, itemId);
|
||||
Optional<Mai2UserItem> userItemOptional = userItemRepository.findByUserAndItemKindAndItemId(profile, itemKind, itemId);
|
||||
|
||||
UserItem userItem;
|
||||
Mai2UserItem userItem;
|
||||
if (userItemOptional.isPresent()) {
|
||||
userItem = userItemOptional.get();
|
||||
} else {
|
||||
userItem = new UserItem(profile);
|
||||
userItem = new Mai2UserItem();
|
||||
userItem.setUser(profile);
|
||||
userItem.setItemId(itemId);
|
||||
userItem.setItemKind(itemKind);
|
||||
}
|
||||
@@ -229,34 +201,34 @@ public class ApiMaimai2PlayerDataController {
|
||||
}
|
||||
|
||||
@GetMapping("recent")
|
||||
public ReducedPageResponse<UserPlaylog> getRecent(@RequestParam long aimeId,
|
||||
@RequestParam(required = false, defaultValue = "0") int page,
|
||||
@RequestParam(required = false, defaultValue = "10") int size) {
|
||||
Page<UserPlaylog> playlogs = userPlaylogRepository.findByUser_Card_ExtId(aimeId, PageRequest.of(page, size, Sort.Direction.DESC, "id"));
|
||||
public ReducedPageResponse<Mai2UserPlaylog> getRecent(@RequestParam long aimeId,
|
||||
@RequestParam(required = false, defaultValue = "0") int page,
|
||||
@RequestParam(required = false, defaultValue = "10") int size) {
|
||||
Page<Mai2UserPlaylog> playlogs = userPlaylogRepository.findByUser_Card_ExtId(aimeId, PageRequest.of(page, size, Sort.Direction.DESC, "id"));
|
||||
return new ReducedPageResponse<>(playlogs.getContent(), playlogs.getPageable().getPageNumber(), playlogs.getTotalPages(), playlogs.getTotalElements());
|
||||
|
||||
}
|
||||
|
||||
@GetMapping("song/{id}")
|
||||
public List<UserMusicDetail> getSongDetail(@RequestParam long aimeId, @PathVariable int id) {
|
||||
public List<Mai2UserMusicDetail> getSongDetail(@RequestParam long aimeId, @PathVariable int id) {
|
||||
return userMusicDetailRepository.findByUser_Card_ExtIdAndMusicId(aimeId, id);
|
||||
}
|
||||
|
||||
@GetMapping("song/{id}/{level}")
|
||||
public List<UserPlaylog> getLevelPlaylog(@RequestParam long aimeId, @PathVariable int id, @PathVariable int level) {
|
||||
public List<Mai2UserPlaylog> getLevelPlaylog(@RequestParam long aimeId, @PathVariable int id, @PathVariable int level) {
|
||||
return userPlaylogRepository.findByUser_Card_ExtIdAndMusicIdAndLevel(aimeId, id, level);
|
||||
}
|
||||
|
||||
@GetMapping("options")
|
||||
public UserOption getOptions(@RequestParam long aimeId) {
|
||||
return userOptionRepository.findByUser_Card_ExtId(aimeId).orElseThrow();
|
||||
public Mai2UserOption getOptions(@RequestParam long aimeId) {
|
||||
return userOptionRepository.findSingleByUser_Card_ExtId(aimeId).orElseThrow();
|
||||
}
|
||||
|
||||
@PostMapping("options")
|
||||
public ResponseEntity<Object> updateOptions(@RequestBody Map<String, Object> request) {
|
||||
UserDetail profile = userDataRepository.findByCard_ExtId(((Number) request.get("aimeId")).longValue()).orElseThrow();
|
||||
Mai2UserDetail profile = userDataRepository.findByCardExtId(((Number) request.get("aimeId")).longValue()).orElseThrow();
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
UserOption userOption = objectMapper.convertValue(request.get("options"), UserOption.class);
|
||||
Mai2UserOption userOption = objectMapper.convertValue(request.get("options"), Mai2UserOption.class);
|
||||
userOption.setUser(profile);
|
||||
userOptionRepository.deleteByUser(profile);
|
||||
userOptionRepository.flush();
|
||||
@@ -265,24 +237,26 @@ public class ApiMaimai2PlayerDataController {
|
||||
|
||||
@GetMapping("general")
|
||||
public ResponseEntity<Object> getGeneralData(@RequestParam long aimeId, @RequestParam String key) {
|
||||
Optional<UserGeneralData> userGeneralDataOptional = userGeneralDataRepository.findByUser_Card_ExtIdAndPropertyKey(aimeId, key);
|
||||
Optional<Mai2UserGeneralData> userGeneralDataOptional = userGeneralDataRepository.findByUser_Card_ExtIdAndPropertyKey(aimeId, key);
|
||||
return userGeneralDataOptional.<ResponseEntity<Object>>map(ResponseEntity::ok)
|
||||
.orElseGet(() -> ResponseEntity.status(HttpStatus.NOT_FOUND).body(new MessageResponse("User or value not found.")));
|
||||
}
|
||||
|
||||
@PostMapping("general")
|
||||
public ResponseEntity<Object> setGeneralData(@RequestBody Map<String, Object> request) {
|
||||
UserDetail profile = userDataRepository.findByCard_ExtId(((Number) request.get("aimeId")).longValue()).orElseThrow();
|
||||
Mai2UserDetail profile = userDataRepository.findByCardExtId(((Number) request.get("aimeId")).longValue()).orElseThrow();
|
||||
String key = (String) request.get("key");
|
||||
String value = (String) request.get("value");
|
||||
|
||||
Optional<UserGeneralData> userGeneralDataOptional = userGeneralDataRepository.findByUserAndPropertyKey(profile, key);
|
||||
UserGeneralData userGeneralData;
|
||||
Optional<Mai2UserGeneralData> userGeneralDataOptional = userGeneralDataRepository.findByUserAndPropertyKey(profile, key);
|
||||
Mai2UserGeneralData userGeneralData;
|
||||
if (userGeneralDataOptional.isPresent()) {
|
||||
userGeneralData = userGeneralDataOptional.get();
|
||||
}
|
||||
else {
|
||||
userGeneralData = new UserGeneralData(profile, key);
|
||||
userGeneralData = new Mai2UserGeneralData();
|
||||
userGeneralData.setUser(profile);
|
||||
userGeneralData.setPropertyKey(key);
|
||||
}
|
||||
userGeneralData.setPropertyValue(value);
|
||||
|
||||
@@ -294,16 +268,16 @@ public class ApiMaimai2PlayerDataController {
|
||||
Maimai2DataExport data = new Maimai2DataExport();
|
||||
try {
|
||||
data.setGameId("SDEZ");
|
||||
data.setUserData(userDataRepository.findByCard_ExtId(aimeId).orElseThrow());
|
||||
data.setUserExtend(userExtendRepository.findByUser_Card_ExtId(aimeId).orElseThrow());
|
||||
data.setUserOption(userOptionRepository.findByUser_Card_ExtId(aimeId).orElseThrow());
|
||||
data.setUserUdemae(userUdemaeRepository.findByUser_Card_ExtId(aimeId).orElseThrow());
|
||||
data.setUserData(userDataRepository.findByCardExtId(aimeId).orElseThrow());
|
||||
data.setUserExtend(userExtendRepository.findSingleByUser_Card_ExtId(aimeId).orElseThrow());
|
||||
data.setUserOption(userOptionRepository.findSingleByUser_Card_ExtId(aimeId).orElseThrow());
|
||||
data.setUserUdemae(userUdemaeRepository.findSingleByUser_Card_ExtId(aimeId).orElseThrow());
|
||||
data.setUserCharacterList(userCharacterRepository.findByUser_Card_ExtId(aimeId));
|
||||
data.setUserGeneralDataList(userGeneralDataRepository.findByUser_Card_ExtId(aimeId));
|
||||
data.setUserItemList(userItemRepository.findByUser_Card_ExtId(aimeId));
|
||||
data.setUserLoginBonusList(userLoginBonusRepository.findByUser_Card_ExtId(aimeId));
|
||||
data.setUserMusicDetailList(userMusicDetailRepository.findByUser_Card_ExtId(aimeId));
|
||||
data.setUserPlaylogList(userPlaylogRepository.findByUser_Card_ExtId(aimeId));
|
||||
data.setUserPlaylogList(userPlaylogRepository.findByUserCardExtId(aimeId));
|
||||
data.setMapEncountNpcList(mapEncountNpcRepository.findByUser_Card_ExtId(aimeId));
|
||||
data.setUserActList(userActRepository.findByUser_Card_ExtId(aimeId));
|
||||
data.setUserChargeList(userChargeRepository.findByUser_Card_ExtId(aimeId));
|
||||
@@ -336,7 +310,7 @@ public class ApiMaimai2PlayerDataController {
|
||||
Card card;
|
||||
if (cardOptional.isPresent()) {
|
||||
card = cardOptional.get();
|
||||
Optional<UserDetail> existUserData = userDataRepository.findByCard(cardOptional.get());
|
||||
Optional<Mai2UserDetail> existUserData = Optional.ofNullable(userDataRepository.findByCard(cardOptional.get()));
|
||||
if (existUserData.isPresent()) {
|
||||
// return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
// .body(new MessageResponse("This card already has a maimai2 profile."));
|
||||
@@ -383,7 +357,7 @@ public class ApiMaimai2PlayerDataController {
|
||||
card = cardService.registerByAccessCode(exUser.getAccessCode());
|
||||
}
|
||||
|
||||
UserDetail userData = mapper.convert(exUser, new TypeReference<>() {
|
||||
Mai2UserDetail userData = mapper.convert(exUser, new TypeReference<>() {
|
||||
});
|
||||
userData.setCard(card);
|
||||
userDataRepository.saveAndFlush(userData);
|
||||
@@ -402,15 +376,15 @@ public class ApiMaimai2PlayerDataController {
|
||||
userChargeRepository.saveAll(data.getUserChargeList().stream().peek(x -> x.setUser(userData)).collect(Collectors.toList()));
|
||||
userCourseRepository.saveAll(data.getUserCourseList().stream().peek(x -> x.setUser(userData)).collect(Collectors.toList()));
|
||||
|
||||
UserExtend userExtend = data.getUserExtend();
|
||||
Mai2UserExtend userExtend = data.getUserExtend();
|
||||
userExtend.setUser(userData);
|
||||
userExtendRepository.save(userExtend);
|
||||
|
||||
UserOption userOption = data.getUserOption();
|
||||
Mai2UserOption userOption = data.getUserOption();
|
||||
userOption.setUser(userData);
|
||||
userOptionRepository.save(userOption);
|
||||
|
||||
UserUdemae userUdemae = data.getUserUdemae();
|
||||
Mai2UserUdemae userUdemae = data.getUserUdemae();
|
||||
userUdemae.setUser(userData);
|
||||
userUdemaeRepository.save(userUdemae);
|
||||
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
package icu.samnyan.aqua.api.controller.sega.game.maimai2
|
||||
|
||||
import ext.*
|
||||
import icu.samnyan.aqua.sega.maimai2.dao.userdata.UserDataRepository
|
||||
import icu.samnyan.aqua.sega.maimai2.dao.userdata.UserGeneralDataRepository
|
||||
import icu.samnyan.aqua.sega.maimai2.dao.userdata.UserPlaylogRepository
|
||||
import org.springframework.http.HttpStatus.*
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
@RestController
|
||||
@RequestMapping("api/game/maimai2new")
|
||||
class Maimai2New(
|
||||
private val userPlaylogRepository: UserPlaylogRepository,
|
||||
private val userDataRepository: UserDataRepository,
|
||||
private val userGeneralDataRepository: UserGeneralDataRepository
|
||||
)
|
||||
{
|
||||
data class TrendOut(val date: String, val rating: Int, val plays: Int)
|
||||
|
||||
@GetMapping("trend")
|
||||
fun trend(@RequestParam userId: Long): List<TrendOut> {
|
||||
// O(n log n) sort
|
||||
val d = userPlaylogRepository.findByUser_Card_ExtId(userId).sortedBy { it.playDate }.toList()
|
||||
|
||||
// Precompute the play counts for each date in O(n)
|
||||
val playCounts = d.groupingBy { it.playDate }.eachCount()
|
||||
|
||||
// Use the precomputed play counts
|
||||
return d.distinctBy { it.playDate }
|
||||
.map { TrendOut(it.playDate, it.afterRating, playCounts[it.playDate] ?: 0) }
|
||||
.sortedBy { it.date }
|
||||
}
|
||||
|
||||
private val shownRanks = listOf(
|
||||
100.5 to "SSS+",
|
||||
100.0 to "SSS",
|
||||
99.5 to "SS+",
|
||||
99.0 to "SS",
|
||||
98.0 to "S+",
|
||||
97.0 to "S").map { (k, v) -> (k * 10000).toInt() to v }
|
||||
|
||||
@GetMapping("user-summary")
|
||||
fun userSummary(@RequestParam userId: Long): Map<String, Any> {
|
||||
// Summary values: total plays, player rating, server-wide ranking
|
||||
// number of each rank, max combo, number of full combo, number of all perfect
|
||||
val user = userDataRepository.findByCard_ExtId(userId).getOrNull() ?: NOT_FOUND()
|
||||
val plays = userPlaylogRepository.findByUser_Card_ExtId(userId)
|
||||
val extra = userGeneralDataRepository.findByUser_Card_ExtId(userId)
|
||||
.associate { it.propertyKey to it.propertyValue }
|
||||
|
||||
// O(6n) ranks algorithm: Loop through the entire list of plays,
|
||||
// count the number of each rank
|
||||
val ranks = shownRanks.associate { (_, v) -> v to 0 }.toMutableMap()
|
||||
plays.forEach {
|
||||
shownRanks.find { (s, _) -> it.achievement > s }?.let { (_, v) -> ranks[v] = ranks[v]!! + 1 }
|
||||
}
|
||||
|
||||
return mapOf(
|
||||
"name" to user.userName,
|
||||
"iconId" to user.iconId,
|
||||
|
||||
"serverRank" to userDataRepository.getRanking(user.playerRating),
|
||||
"accuracy" to plays.sumOf { it.achievement } / plays.size,
|
||||
"rating" to user.playerRating,
|
||||
"ratingHighest" to user.highestRating,
|
||||
"ranks" to ranks.map { (k, v) -> mapOf("name" to k, "count" to v) },
|
||||
"maxCombo" to plays.maxOf { it.maxCombo },
|
||||
"fullCombo" to plays.count { it.totalCombo == it.maxCombo },
|
||||
"allPerfect" to plays.count { it.achievement == 1010000 },
|
||||
"totalDxScore" to user.totalDeluxscore,
|
||||
|
||||
"plays" to plays.size,
|
||||
"totalPlayTime" to plays.count() * 3, // TODO: Make a more accurate estimate
|
||||
"joined" to user.firstPlayDate,
|
||||
"lastSeen" to user.lastPlayDate,
|
||||
"lastVersion" to user.lastRomVersion,
|
||||
|
||||
"best35" to (extra["recent_rating"] ?: ""),
|
||||
"best15" to (extra["recent_rating_new"] ?: ""),
|
||||
|
||||
"recent" to plays.sortedBy { it.playDate }.takeLast(10)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package icu.samnyan.aqua.api.controller.sega.game.ongeki;
|
||||
|
||||
import icu.samnyan.aqua.sega.ongeki.dao.gamedata.*;
|
||||
import icu.samnyan.aqua.sega.ongeki.model.gamedata.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
@@ -11,6 +12,7 @@ import java.util.List;
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("api/game/ongeki/data")
|
||||
@AllArgsConstructor
|
||||
public class ApiOngekiGameDataController {
|
||||
|
||||
private final GameCardRepository gameCardRepository;
|
||||
@@ -19,14 +21,6 @@ public class ApiOngekiGameDataController {
|
||||
private final GameMusicRepository gameMusicRepository;
|
||||
private final GameSkillRepository gameSkillRepository;
|
||||
|
||||
public ApiOngekiGameDataController(GameCardRepository gameCardRepository, GameCharaRepository gameCharaRepository, GameEventRepository gameEventRepository, GameMusicRepository gameMusicRepository, GameSkillRepository gameSkillRepository) {
|
||||
this.gameCardRepository = gameCardRepository;
|
||||
this.gameCharaRepository = gameCharaRepository;
|
||||
this.gameEventRepository = gameEventRepository;
|
||||
this.gameMusicRepository = gameMusicRepository;
|
||||
this.gameSkillRepository = gameSkillRepository;
|
||||
}
|
||||
|
||||
@GetMapping("cardList")
|
||||
public List<GameCard> getCardList() {
|
||||
return gameCardRepository.findAll();
|
||||
|
||||
@@ -16,6 +16,7 @@ import icu.samnyan.aqua.sega.ongeki.dao.userdata.*;
|
||||
import icu.samnyan.aqua.sega.ongeki.model.gamedata.GameCard;
|
||||
import icu.samnyan.aqua.sega.ongeki.model.response.data.UserRivalData;
|
||||
import icu.samnyan.aqua.sega.ongeki.model.userdata.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
@@ -35,6 +36,7 @@ import java.util.stream.Collectors;
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("api/game/ongeki")
|
||||
@AllArgsConstructor
|
||||
public class ApiOngekiPlayerDataController {
|
||||
|
||||
private final ApiMapper mapper;
|
||||
@@ -76,38 +78,6 @@ public class ApiOngekiPlayerDataController {
|
||||
|
||||
private final GameCardRepository gameCardRepository;
|
||||
|
||||
public ApiOngekiPlayerDataController(ApiMapper mapper, CardService cardService, UserRivalDataRepository userRivalDataRepository, UserActivityRepository userActivityRepository, UserCardRepository userCardRepository, UserChapterRepository userChapterRepository, UserCharacterRepository userCharacterRepository, UserDataRepository userDataRepository, UserDeckRepository userDeckRepository, UserEventPointRepository userEventPointRepository, UserItemRepository userItemRepository, UserLoginBonusRepository userLoginBonusRepository, UserMissionPointRepository userMissionPointRepository, UserMusicDetailRepository userMusicDetailRepository, UserMusicItemRepository userMusicItemRepository, UserOptionRepository userOptionRepository, UserPlaylogRepository userPlaylogRepository, UserStoryRepository userStoryRepository, UserTrainingRoomRepository userTrainingRoomRepository, UserGeneralDataRepository userGeneralDataRepository, GameCardRepository gameCardRepository, UserTradeItemRepository userTradeItemRepository, UserEventMusicRepository userEventMusicRepository, UserTechEventRepository userTechEventRepository, UserKopRepository userKopRepository, UserMemoryChapterRepository userMemoryChapterRepository, UserScenarioRepository userScenarioRepository, UserBossRepository userBossRepository, UserTechCountRepository userTechCountRepository) {
|
||||
this.mapper = mapper;
|
||||
this.cardService = cardService;
|
||||
this.userActivityRepository = userActivityRepository;
|
||||
this.userCardRepository = userCardRepository;
|
||||
this.userChapterRepository = userChapterRepository;
|
||||
this.userCharacterRepository = userCharacterRepository;
|
||||
this.userDataRepository = userDataRepository;
|
||||
this.userDeckRepository = userDeckRepository;
|
||||
this.userEventPointRepository = userEventPointRepository;
|
||||
this.userItemRepository = userItemRepository;
|
||||
this.userLoginBonusRepository = userLoginBonusRepository;
|
||||
this.userMissionPointRepository = userMissionPointRepository;
|
||||
this.userMusicDetailRepository = userMusicDetailRepository;
|
||||
this.userMusicItemRepository = userMusicItemRepository;
|
||||
this.userOptionRepository = userOptionRepository;
|
||||
this.userPlaylogRepository = userPlaylogRepository;
|
||||
this.userStoryRepository = userStoryRepository;
|
||||
this.userTrainingRoomRepository = userTrainingRoomRepository;
|
||||
this.userGeneralDataRepository = userGeneralDataRepository;
|
||||
this.gameCardRepository = gameCardRepository;
|
||||
this.userTradeItemRepository = userTradeItemRepository;
|
||||
this.userEventMusicRepository = userEventMusicRepository;
|
||||
this.userTechEventRepository = userTechEventRepository;
|
||||
this.userKopRepository = userKopRepository;
|
||||
this.userMemoryChapterRepository = userMemoryChapterRepository;
|
||||
this.userScenarioRepository = userScenarioRepository;
|
||||
this.userBossRepository = userBossRepository;
|
||||
this.userTechCountRepository = userTechCountRepository;
|
||||
this.userRivalDataRepository = userRivalDataRepository;
|
||||
}
|
||||
|
||||
@GetMapping("profile")
|
||||
public ProfileResp getProfile(@RequestParam long aimeId) {
|
||||
return mapper.convert(userDataRepository.findByCard_ExtId(aimeId).orElseThrow(), new TypeReference<>() {
|
||||
@@ -348,7 +318,7 @@ public class ApiOngekiPlayerDataController {
|
||||
|
||||
var rivalDataList = userDataRepository.findByCard_ExtIdIn(rivalUserIds)
|
||||
.stream()
|
||||
.map(x -> new UserRivalData(x.getCard().getExtId().longValue(), x.getUserName()))
|
||||
.map(x -> new UserRivalData(x.getCard().getExtId(), x.getUserName()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return rivalDataList;
|
||||
@@ -455,7 +425,7 @@ public class ApiOngekiPlayerDataController {
|
||||
Card card;
|
||||
if (cardOptional.isPresent()) {
|
||||
card = cardOptional.get();
|
||||
Optional<UserData> existUserData = userDataRepository.findByCard(cardOptional.get());
|
||||
Optional<UserData> existUserData = Optional.ofNullable(userDataRepository.findByCard(cardOptional.get()));
|
||||
if (existUserData.isPresent()) {
|
||||
// return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
// .body(new MessageResponse("This card already has a ongeki profile."));
|
||||
|
||||
@@ -7,6 +7,7 @@ import icu.samnyan.aqua.sega.chunithm.model.userdata.UserMusicDetail;
|
||||
import icu.samnyan.aqua.sega.chunithm.service.GameMusicService;
|
||||
import icu.samnyan.aqua.sega.chunithm.service.UserDataService;
|
||||
import icu.samnyan.aqua.sega.chunithm.service.UserMusicDetailService;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -24,6 +25,7 @@ import java.util.Optional;
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("api/manage/chuni/v1")
|
||||
@AllArgsConstructor
|
||||
public class ApiChuniV1ManageController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ApiChuniV1ManageController.class);
|
||||
@@ -34,12 +36,6 @@ public class ApiChuniV1ManageController {
|
||||
|
||||
private final GameMusicService gameMusicService;
|
||||
|
||||
public ApiChuniV1ManageController(UserDataService userDataService, UserMusicDetailService userMusicDetailService, GameMusicService gameMusicService) {
|
||||
this.userDataService = userDataService;
|
||||
this.userMusicDetailService = userMusicDetailService;
|
||||
this.gameMusicService = gameMusicService;
|
||||
}
|
||||
|
||||
/**
|
||||
* A request to fill fake score to all chart. only use for testing
|
||||
* @param aimeId The internal id of a card
|
||||
|
||||
@@ -10,6 +10,7 @@ import icu.samnyan.aqua.sega.diva.model.common.Edition;
|
||||
import icu.samnyan.aqua.sega.diva.model.gamedata.*;
|
||||
import icu.samnyan.aqua.sega.general.dao.PropertyEntryRepository;
|
||||
import icu.samnyan.aqua.sega.general.model.PropertyEntry;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -21,6 +22,7 @@ import java.util.Optional;
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("api/manage/diva/")
|
||||
@AllArgsConstructor
|
||||
public class ApiDivaManageController {
|
||||
|
||||
private final PvEntryRepository pvEntryRepository;
|
||||
@@ -29,17 +31,6 @@ public class ApiDivaManageController {
|
||||
private final FestaRepository festaRepository;
|
||||
private final ContestRepository contestRepository;
|
||||
private final PropertyEntryRepository propertyEntryRepository;
|
||||
private final DivaPvRepository divaPvRepository;
|
||||
|
||||
public ApiDivaManageController(PvEntryRepository pvEntryRepository, DivaModuleRepository moduleRepository, DivaCustomizeRepository customizeRepository, FestaRepository festaRepository, ContestRepository contestRepository, PropertyEntryRepository propertyEntryRepository, DivaPvRepository divaPvRepository) {
|
||||
this.pvEntryRepository = pvEntryRepository;
|
||||
this.moduleRepository = moduleRepository;
|
||||
this.customizeRepository = customizeRepository;
|
||||
this.festaRepository = festaRepository;
|
||||
this.contestRepository = contestRepository;
|
||||
this.propertyEntryRepository = propertyEntryRepository;
|
||||
this.divaPvRepository = divaPvRepository;
|
||||
}
|
||||
|
||||
@PostMapping("pvList")
|
||||
public List<PvEntry> updatePvList(@RequestBody PvListRequest request) {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package icu.samnyan.aqua.api.model;
|
||||
|
||||
import icu.samnyan.aqua.api.model.MessageResponse;
|
||||
|
||||
public class ObjectMessageResponse<T> extends MessageResponse {
|
||||
private T data;
|
||||
|
||||
|
||||
23
src/main/java/icu/samnyan/aqua/api/model/resp/sega/chuni/v2/external/Chu3DataExport.kt
vendored
Normal file
23
src/main/java/icu/samnyan/aqua/api/model/resp/sega/chuni/v2/external/Chu3DataExport.kt
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
package icu.samnyan.aqua.api.model.resp.sega.chuni.v2.external
|
||||
|
||||
import icu.samnyan.aqua.net.games.IExportClass
|
||||
import icu.samnyan.aqua.sega.chusan.model.userdata.*
|
||||
|
||||
data class Chu3DataExport(
|
||||
override var gameId: String = "SDHD",
|
||||
override var userData: Chu3UserData,
|
||||
var userGameOption: UserGameOption,
|
||||
var userActivityList: List<UserActivity>,
|
||||
var userCharacterList: List<UserCharacter>,
|
||||
var userChargeList: List<UserCharge>,
|
||||
var userCourseList: List<UserCourse>,
|
||||
var userDuelList: List<UserDuel>,
|
||||
var userItemList: List<UserItem>,
|
||||
var userMapList: List<UserMap>,
|
||||
var userMusicDetailList: List<UserMusicDetail>,
|
||||
var userPlaylogList: List<UserPlaylog>,
|
||||
): IExportClass<Chu3UserData> {
|
||||
constructor() : this("SDHD",
|
||||
Chu3UserData(), UserGameOption(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList())
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
package icu.samnyan.aqua.api.model.resp.sega.chuni.v2.external;
|
||||
|
||||
import icu.samnyan.aqua.sega.chusan.model.userdata.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This class is use for exporting chusan profile
|
||||
* @author samnyan (privateamusement@protonmail.com)
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class ChuniDataExport {
|
||||
private String gameId = "SDHD";
|
||||
private UserData userData;
|
||||
private List<UserActivity> userActivityList;
|
||||
private List<UserCharacter> userCharacterList;
|
||||
private List<UserCharge> userChargeList;
|
||||
private List<UserCourse> userCourseList;
|
||||
private List<UserDuel> userDuelList;
|
||||
private UserGameOption userGameOption;
|
||||
private List<UserItem> userItemList;
|
||||
private List<UserMapArea> userMapList;
|
||||
private List<UserMusicDetail> userMusicDetailList;
|
||||
private List<UserPlaylog> userPlaylogList;
|
||||
}
|
||||
@@ -24,7 +24,7 @@ public class ChuniDataImport {
|
||||
private List<UserDuel> userDuelList;
|
||||
private UserGameOption userGameOption;
|
||||
private List<UserItem> userItemList;
|
||||
private List<UserMapArea> userMapList;
|
||||
private List<UserMap> userMapList;
|
||||
private List<UserMusicDetail> userMusicDetailList;
|
||||
private List<UserPlaylog> userPlaylogList;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import lombok.Data;
|
||||
*/
|
||||
@Data
|
||||
public class PlayerInfo {
|
||||
private int pdId;
|
||||
private long pdId;
|
||||
private String playerName;
|
||||
private int vocaloidPoints;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package icu.samnyan.aqua.api.model.resp.sega.maimai2;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author samnyan (privateamusement@protonmail.com)
|
||||
*/
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
package icu.samnyan.aqua.api.model.resp.sega.maimai2.external;
|
||||
|
||||
import icu.samnyan.aqua.sega.maimai2.model.userdata.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author samnyan (privateamusement@protonmail.com)
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class Maimai2DataExport {
|
||||
private String gameId = "SDEZ";
|
||||
private UserDetail userData;
|
||||
private UserExtend userExtend;
|
||||
private UserOption userOption;
|
||||
private List<MapEncountNpc> mapEncountNpcList;
|
||||
private List<UserAct> userActList;
|
||||
private List<UserCharacter> userCharacterList;
|
||||
private List<UserCharge> userChargeList;
|
||||
private List<UserCourse> userCourseList;
|
||||
private List<UserFavorite> userFavoriteList;
|
||||
private List<UserFriendSeasonRanking> userFriendSeasonRankingList;
|
||||
private List<UserGeneralData> userGeneralDataList;
|
||||
private List<UserGhost> userGhostList;
|
||||
private List<UserItem> userItemList;
|
||||
private List<UserLoginBonus> userLoginBonusList;
|
||||
private List<UserMap> userMapList;
|
||||
private List<UserMusicDetail> userMusicDetailList;
|
||||
private List<UserPlaylog> userPlaylogList;
|
||||
private List<UserRate> userRateList;
|
||||
private UserUdemae userUdemae;
|
||||
}
|
||||
30
src/main/java/icu/samnyan/aqua/api/model/resp/sega/maimai2/external/Maimai2DataExport.kt
vendored
Normal file
30
src/main/java/icu/samnyan/aqua/api/model/resp/sega/maimai2/external/Maimai2DataExport.kt
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
package icu.samnyan.aqua.api.model.resp.sega.maimai2.external
|
||||
|
||||
import icu.samnyan.aqua.net.games.IExportClass
|
||||
import icu.samnyan.aqua.sega.maimai2.model.userdata.*
|
||||
|
||||
data class Maimai2DataExport(
|
||||
override var userData: Mai2UserDetail,
|
||||
var userExtend: Mai2UserExtend,
|
||||
var userOption: Mai2UserOption,
|
||||
var userUdemae: Mai2UserUdemae,
|
||||
var mapEncountNpcList: List<Mai2MapEncountNpc>,
|
||||
var userActList: List<Mai2UserAct>,
|
||||
var userCharacterList: List<Mai2UserCharacter>,
|
||||
var userChargeList: List<Mai2UserCharge>,
|
||||
var userCourseList: List<Mai2UserCourse>,
|
||||
var userFavoriteList: List<Mai2UserFavorite>,
|
||||
var userFriendSeasonRankingList: List<Mai2UserFriendSeasonRanking>,
|
||||
var userGeneralDataList: List<Mai2UserGeneralData>,
|
||||
var userItemList: List<Mai2UserItem>,
|
||||
var userLoginBonusList: List<Mai2UserLoginBonus>,
|
||||
var userMapList: List<Mai2UserMap>,
|
||||
var userMusicDetailList: List<Mai2UserMusicDetail>,
|
||||
var userPlaylogList: List<Mai2UserPlaylog>,
|
||||
override var gameId: String = "SDEZ",
|
||||
): IExportClass<Mai2UserDetail> {
|
||||
constructor() : this(Mai2UserDetail(), Mai2UserExtend(), Mai2UserOption(), Mai2UserUdemae(),
|
||||
mutableListOf(), mutableListOf(), mutableListOf(), mutableListOf(), mutableListOf(), mutableListOf(),
|
||||
mutableListOf(), mutableListOf(), mutableListOf(), mutableListOf(), mutableListOf(), mutableListOf(),
|
||||
mutableListOf())
|
||||
}
|
||||
@@ -16,22 +16,22 @@ import java.util.List;
|
||||
public class Maimai2DataImport {
|
||||
private String gameId;
|
||||
private ExternalUserData userData;
|
||||
private UserExtend userExtend;
|
||||
private UserOption userOption;
|
||||
private List<MapEncountNpc> mapEncountNpcList;
|
||||
private List<UserAct> userActList;
|
||||
private List<UserCharacter> userCharacterList;
|
||||
private List<UserCharge> userChargeList;
|
||||
private List<UserCourse> userCourseList;
|
||||
private List<UserFavorite> userFavoriteList;
|
||||
private List<UserFriendSeasonRanking> userFriendSeasonRankingList;
|
||||
private List<UserGeneralData> userGeneralDataList;
|
||||
private List<UserGhost> userGhostList;
|
||||
private List<UserItem> userItemList;
|
||||
private List<UserLoginBonus> userLoginBonusList;
|
||||
private List<UserMap> userMapList;
|
||||
private List<UserMusicDetail> userMusicDetailList;
|
||||
private List<UserPlaylog> userPlaylogList;
|
||||
private List<UserRate> userRateList;
|
||||
private UserUdemae userUdemae;
|
||||
private Mai2UserExtend userExtend;
|
||||
private Mai2UserOption userOption;
|
||||
private List<Mai2MapEncountNpc> mapEncountNpcList;
|
||||
private List<Mai2UserAct> userActList;
|
||||
private List<Mai2UserCharacter> userCharacterList;
|
||||
private List<Mai2UserCharge> userChargeList;
|
||||
private List<Mai2UserCourse> userCourseList;
|
||||
private List<Mai2UserFavorite> userFavoriteList;
|
||||
private List<Mai2UserFriendSeasonRanking> userFriendSeasonRankingList;
|
||||
private List<Mai2UserGeneralData> userGeneralDataList;
|
||||
private List<Mai2UserGhost> userGhostList;
|
||||
private List<Mai2UserItem> userItemList;
|
||||
private List<Mai2UserLoginBonus> userLoginBonusList;
|
||||
private List<Mai2UserMap> userMapList;
|
||||
private List<Mai2UserMusicDetail> userMusicDetailList;
|
||||
private List<Mai2UserPlaylog> userPlaylogList;
|
||||
private List<Mai2UserRate> userRateList;
|
||||
private Mai2UserUdemae userUdemae;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
package icu.samnyan.aqua.api.model.resp.sega.ongeki.external;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import icu.samnyan.aqua.sega.general.model.Card;
|
||||
import icu.samnyan.aqua.sega.util.jackson.AccessCodeSerializer;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,7 +6,6 @@ import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author samnyan (privateamusement@protonmail.com)
|
||||
|
||||
@@ -6,7 +6,6 @@ import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author samnyan (privateamusement@protonmail.com)
|
||||
|
||||
210
src/main/java/icu/samnyan/aqua/net/CardController.kt
Normal file
210
src/main/java/icu/samnyan/aqua/net/CardController.kt
Normal file
@@ -0,0 +1,210 @@
|
||||
package icu.samnyan.aqua.net
|
||||
|
||||
import ext.*
|
||||
import icu.samnyan.aqua.net.components.JWT
|
||||
import icu.samnyan.aqua.net.db.AquaUserServices
|
||||
import icu.samnyan.aqua.net.games.GenericUserDataRepo
|
||||
import icu.samnyan.aqua.net.games.IUserData
|
||||
import icu.samnyan.aqua.net.utils.AquaNetProps
|
||||
import icu.samnyan.aqua.net.utils.SUCCESS
|
||||
import icu.samnyan.aqua.sega.chusan.model.Chu3UserDataRepo
|
||||
import icu.samnyan.aqua.sega.general.dao.CardRepository
|
||||
import icu.samnyan.aqua.sega.general.model.Card
|
||||
import icu.samnyan.aqua.sega.general.service.CardService
|
||||
import icu.samnyan.aqua.sega.maimai2.model.Mai2UserDataRepo
|
||||
import icu.samnyan.aqua.sega.wacca.model.db.WcUserRepo
|
||||
import org.springframework.scheduling.annotation.Scheduled
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
@RestController
|
||||
@API("/api/v2/card")
|
||||
class CardController(
|
||||
val jwt: JWT,
|
||||
val us: AquaUserServices,
|
||||
val cardService: CardService,
|
||||
val cardGameService: CardGameService,
|
||||
val cardRepository: CardRepository,
|
||||
val props: AquaNetProps
|
||||
) {
|
||||
@API("/summary")
|
||||
@Doc("Get a summary of the card, including the user's name, rating, and last login date.", "Summary of the card")
|
||||
suspend fun summary(@RP cardId: Str): Any
|
||||
{
|
||||
// DO NOT CHANGE THIS ERROR MESSAGE - The frontend uses it to detect if the card is not found
|
||||
val card = cardService.tryLookup(cardId) ?: (404 - "Card not found")
|
||||
|
||||
// Lookup data for each game
|
||||
return mapOf(
|
||||
"card" to card,
|
||||
"summary" to cardGameService.getSummary(card),
|
||||
)
|
||||
}
|
||||
|
||||
@API("/user-games")
|
||||
@Doc("Get the game summary of the user, including the user's name, rating, and last login date.", "Summary of the user")
|
||||
suspend fun userGames(@RP username: Str) = us.cardByName(username) { card -> cardGameService.getSummary(card) }
|
||||
|
||||
/**
|
||||
* Bind a card to the user. This action will migrate selected data from the card to the user's ghost card.
|
||||
*
|
||||
* Non-migrated data will not be lost, but will be inaccessible from the card until the card is unbound.
|
||||
*
|
||||
* @param token JWT token
|
||||
* @param cardId Card ID
|
||||
* @param migrate Things to migrate, stored as a comma-separated list of game IDs (e.g. "maimai2,chusan")
|
||||
*/
|
||||
@API("/link")
|
||||
@Doc("Bind a card to the user. This action will migrate selected data from the card to the user's ghost card.", "Success message")
|
||||
suspend fun link(@RP token: Str, @RP cardId: Str, @RP migrate: Str) = jwt.auth(token) { u ->
|
||||
// Check if the user's card limit is reached
|
||||
if (u.cards.size >= props.linkCardLimit) 400 - "Card limit reached"
|
||||
|
||||
// Try to look up the card
|
||||
val card = cardService.tryLookup(cardId)
|
||||
|
||||
// If no card is found, create a new card
|
||||
if (card == null) {
|
||||
// Ensure the format of the card ID is correct
|
||||
val id = cardService.sanitizeCardId(cardId)
|
||||
|
||||
// Create a new card
|
||||
cardService.registerByAccessCode(id, u)
|
||||
|
||||
return SUCCESS
|
||||
}
|
||||
|
||||
// If card is already bound
|
||||
if (card.aquaUser != null) 400 - "Card already bound to another user"
|
||||
|
||||
// Bind the card
|
||||
card.aquaUser = u
|
||||
async { cardRepository.save(card) }
|
||||
|
||||
// Migrate selected data to the new user
|
||||
val games = migrate.split(',')
|
||||
cardGameService.migrate(card, games)
|
||||
|
||||
SUCCESS
|
||||
}
|
||||
|
||||
@API("/unlink")
|
||||
@Doc("Unbind a card from the user. No data will be migrated during this action.", "Success message")
|
||||
suspend fun unlink(@RP token: Str, @RP cardId: Str) = jwt.auth(token) { u ->
|
||||
// Try to look up the card
|
||||
val card = cardService.tryLookup(cardId) ?: (404 - "Card not found")
|
||||
|
||||
// If the card is not bound to the user
|
||||
if (card.aquaUser != u) 400 - "Card not linked to user"
|
||||
|
||||
// Ghost cards cannot be unlinked
|
||||
if (card.isGhost) 400 - "Account virtual cards cannot be unlinked"
|
||||
|
||||
// Unbind the card
|
||||
card.aquaUser = null
|
||||
async { cardRepository.save(card) }
|
||||
|
||||
SUCCESS
|
||||
}
|
||||
|
||||
@API("/default-game")
|
||||
@Doc("Get the default game for the card.", "Game ID")
|
||||
suspend fun defaultGame(@RP username: Str) = us.cardByName(username) { card ->
|
||||
mapOf("game" to cardGameService.getSummary(card).filterValues { it != null }.keys.firstOrNull())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate data from the card to the user's ghost card
|
||||
*
|
||||
* Assumption: The card is already linked to the user.
|
||||
*/
|
||||
suspend fun <T : IUserData> migrateCard(repo: GenericUserDataRepo<T>, card: Card): Bool
|
||||
{
|
||||
// Check if data already exists in the user's ghost card
|
||||
async { repo.findByCard(card.aquaUser!!.ghostCard) }?.let {
|
||||
// Unbind the data from the card
|
||||
it.card = null
|
||||
async { repo.save(it) }
|
||||
}
|
||||
|
||||
// Migrate data from the card to the user's ghost card
|
||||
// An easy migration is to change the UserData card field to the user's ghost card
|
||||
val data = async { repo.findByCard(card) } ?: return false
|
||||
data.card = card.aquaUser!!.ghostCard
|
||||
async { repo.save(data) }
|
||||
return true
|
||||
}
|
||||
|
||||
suspend fun getSummaryFor(repo: GenericUserDataRepo<*>, card: Card): Map<Str, Any>?
|
||||
{
|
||||
val data = async { repo.findByCard(card) } ?: return null
|
||||
return mapOf(
|
||||
"name" to data.userName,
|
||||
"rating" to data.playerRating,
|
||||
"lastLogin" to data.lastPlayDate,
|
||||
)
|
||||
}
|
||||
|
||||
@Service
|
||||
class CardGameService(
|
||||
val maimai2: Mai2UserDataRepo,
|
||||
val chusan: Chu3UserDataRepo,
|
||||
val wacca: WcUserRepo,
|
||||
val ongeki: icu.samnyan.aqua.sega.ongeki.dao.userdata.UserDataRepository,
|
||||
val diva: icu.samnyan.aqua.sega.diva.dao.userdata.PlayerProfileRepository,
|
||||
val safety: AquaNetSafetyService,
|
||||
val cardRepo: CardRepository
|
||||
) {
|
||||
companion object {
|
||||
val log = logger()
|
||||
}
|
||||
|
||||
suspend fun migrate(crd: Card, games: List<String>) = async {
|
||||
// Migrate data from the card to the user's ghost card
|
||||
// An easy migration is to change the UserData card field to the user's ghost card
|
||||
games.forEach { game ->
|
||||
when (game) {
|
||||
"mai2" -> migrateCard(maimai2, crd)
|
||||
"chu3" -> migrateCard(chusan, crd)
|
||||
"ongeki" -> migrateCard(ongeki, crd)
|
||||
"wacca" -> migrateCard(wacca, crd)
|
||||
// TODO: diva
|
||||
// "diva" -> diva.findByPdId(card.extId.toInt()).getOrNull()?.let {
|
||||
// it.pdId = card.aquaUser!!.ghostCard
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getSummary(card: Card) = async { mapOf(
|
||||
"mai2" to getSummaryFor(maimai2, card),
|
||||
"chu3" to getSummaryFor(chusan, card),
|
||||
"ongeki" to getSummaryFor(ongeki, card),
|
||||
"wacca" to getSummaryFor(wacca, card),
|
||||
"diva" to diva.findByPdId(card.extId).getOrNull()?.let {
|
||||
mapOf(
|
||||
"name" to it.playerName,
|
||||
"rating" to it.level,
|
||||
)
|
||||
},
|
||||
) }
|
||||
|
||||
// Every hour
|
||||
@Scheduled(fixedDelay = 3600000)
|
||||
suspend fun autoBan() {
|
||||
log.info("Running auto-ban")
|
||||
|
||||
// Ban any players with unacceptable names
|
||||
for (repo in listOf(maimai2, chusan, wacca, ongeki)) {
|
||||
repo.findAll().filter { it.card != null && !it.card!!.rankingBanned }.forEach { data ->
|
||||
if (!safety.isSafe(data.userName)) {
|
||||
log.info("Banning user ${data.userName} ${data.card!!.id}")
|
||||
data.card!!.rankingBanned = true
|
||||
async { cardRepo.save(data.card!!) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
55
src/main/java/icu/samnyan/aqua/net/Frontier.kt
Normal file
55
src/main/java/icu/samnyan/aqua/net/Frontier.kt
Normal file
@@ -0,0 +1,55 @@
|
||||
package icu.samnyan.aqua.net
|
||||
|
||||
import ext.*
|
||||
import icu.samnyan.aqua.sega.general.service.CardService
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "aqua-net.frontier")
|
||||
class FrontierProps {
|
||||
var enabled: Boolean = false
|
||||
var ftk: String = ""
|
||||
}
|
||||
|
||||
@RestController
|
||||
@ConditionalOnProperty("aqua-net.frontier.enabled", havingValue = "true")
|
||||
@API("/api/v2/frontier")
|
||||
class Frontier(
|
||||
val cardService: CardService,
|
||||
val props: FrontierProps
|
||||
) {
|
||||
fun Str.checkFtk() {
|
||||
if (this != props.ftk) 403 - "Invalid FTK"
|
||||
}
|
||||
|
||||
@API("/register-card")
|
||||
@Doc("Register a new card by access code", "Card information")
|
||||
suspend fun registerCard(@RP ftk: Str, @RP accessCode: Str): Any {
|
||||
ftk.checkFtk()
|
||||
|
||||
if (accessCode.length != 20) 400 - "Invalid access code"
|
||||
if (!accessCode.startsWith("9900")) 400 - "Frontier access code must start with 9900"
|
||||
if (async { cardService.cardRepo.findByLuid(accessCode) }.isPresent) 400 - "Card already registered"
|
||||
|
||||
val card = async { cardService.registerByAccessCode(accessCode) }
|
||||
return mapOf(
|
||||
"card" to card,
|
||||
"id" to card.extId // Expose hidden ID
|
||||
)
|
||||
}
|
||||
|
||||
@API("/lookup-card")
|
||||
@Doc("Lookup a card by access code", "Card information")
|
||||
suspend fun lookupCard(@RP ftk: Str, @RP accessCode: Str): Any {
|
||||
ftk.checkFtk()
|
||||
|
||||
val card = cardService.tryLookup(accessCode) ?: (404 - "Card not found")
|
||||
return mapOf(
|
||||
"card" to card,
|
||||
"id" to card.extId // Expose hidden ID
|
||||
)
|
||||
}
|
||||
}
|
||||
74
src/main/java/icu/samnyan/aqua/net/Safety.kt
Normal file
74
src/main/java/icu/samnyan/aqua/net/Safety.kt
Normal file
@@ -0,0 +1,74 @@
|
||||
package icu.samnyan.aqua.net
|
||||
|
||||
import ext.HTTP
|
||||
import ext.async
|
||||
import ext.toJson
|
||||
import icu.samnyan.aqua.net.games.BaseEntity
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.http.*
|
||||
import jakarta.persistence.Entity
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.stereotype.Service
|
||||
import java.text.Normalizer
|
||||
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "aqua-net.openai")
|
||||
class OpenAIConfig {
|
||||
var apiKey: String = ""
|
||||
}
|
||||
|
||||
@Entity
|
||||
class AquaNetSafety : BaseEntity() {
|
||||
var content: String = ""
|
||||
var safe: Boolean = false
|
||||
}
|
||||
|
||||
interface AquaNetSafetyRepo : JpaRepository<AquaNetSafety, Long> {
|
||||
fun findByContent(content: String): AquaNetSafety?
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class OpenAIResp<T>(
|
||||
val id: String,
|
||||
val model: String,
|
||||
val results: List<T>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class OpenAIMod(
|
||||
val flagged: Boolean,
|
||||
val categories: Map<String, Boolean>,
|
||||
val categoryScores: Map<String, Double>,
|
||||
)
|
||||
|
||||
@Service
|
||||
class AquaNetSafetyService(
|
||||
val safety: AquaNetSafetyRepo,
|
||||
val openAIConfig: OpenAIConfig
|
||||
) {
|
||||
suspend fun isSafe(rawContent: String): Boolean {
|
||||
// NFKC normalize
|
||||
val content = Normalizer.normalize(rawContent, Normalizer.Form.NFKC)
|
||||
if (content.isBlank()) return true
|
||||
|
||||
async { safety.findByContent(content) }?.let { return it.safe }
|
||||
|
||||
// Query OpenAI
|
||||
HTTP.post("https://api.openai.com/v1/moderations") {
|
||||
header("Authorization", "Bearer ${openAIConfig.apiKey}")
|
||||
header("Content-Type", "application/json")
|
||||
setBody(mapOf("input" to content).toJson())
|
||||
}.let {
|
||||
if (!it.status.isSuccess()) return true
|
||||
val body = it.body<OpenAIResp<OpenAIMod>>()
|
||||
return AquaNetSafety().apply {
|
||||
this.content = content
|
||||
this.safe = !body.results.first().flagged
|
||||
}.also { safety.save(it) }.safe
|
||||
}
|
||||
}
|
||||
}
|
||||
45
src/main/java/icu/samnyan/aqua/net/SettingsApi.kt
Normal file
45
src/main/java/icu/samnyan/aqua/net/SettingsApi.kt
Normal file
@@ -0,0 +1,45 @@
|
||||
package icu.samnyan.aqua.net
|
||||
|
||||
import ext.*
|
||||
import icu.samnyan.aqua.net.db.AquaGameOptions
|
||||
import icu.samnyan.aqua.net.db.AquaGameOptionsRepo
|
||||
import icu.samnyan.aqua.net.db.AquaNetUserRepo
|
||||
import icu.samnyan.aqua.net.db.AquaUserServices
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import kotlin.reflect.full.findAnnotation
|
||||
import kotlin.reflect.jvm.jvmErasure
|
||||
|
||||
@RestController
|
||||
@API("/api/v2/settings")
|
||||
class SettingsApi(
|
||||
val us: AquaUserServices,
|
||||
val userRepo: AquaNetUserRepo,
|
||||
val goRepo: AquaGameOptionsRepo
|
||||
) {
|
||||
// Get all params with SettingField annotation
|
||||
val fields = AquaGameOptions::class.vars()
|
||||
.mapNotNull { it.findAnnotation<SettingField>()?.let { an -> it to an } }
|
||||
val fieldMap = fields.associate { (f, _) -> f.name to f }
|
||||
val fieldDesc = fields.map { (f, _) -> mapOf(
|
||||
"key" to f.name, "type" to f.returnType.jvmErasure.simpleName
|
||||
) }
|
||||
|
||||
@API("get")
|
||||
@Doc("Get the game options of the logged in user")
|
||||
fun getSettings(@RP token: String) = us.jwt.auth(token) { u ->
|
||||
val go = u.gameOptions ?: AquaGameOptions()
|
||||
fieldDesc.map { it + ("value" to fieldMap[it["key"]]!!.get(go)) }
|
||||
}
|
||||
|
||||
@API("set")
|
||||
@Doc("Set a field in the game options")
|
||||
fun setField(@RP token: String, @RP key: String, @RP value: String) = us.jwt.auth(token) { u ->
|
||||
val field = fieldMap[key] ?: (400 - "Invalid field $key")
|
||||
val options = u.gameOptions ?: AquaGameOptions().also {
|
||||
userRepo.save(u.apply { gameOptions = it })
|
||||
}
|
||||
// Check field type
|
||||
field.setCast(options, value)
|
||||
goRepo.save(options)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user