619 Commits
master ... tmp

Author SHA1 Message Date
Azalea
85296ae3b1 [O] WIP 2024-04-20 09:33:53 +09:00
Azalea
8e882aafa1 [F] Fix register error not displaying 2024-04-19 16:05:57 +09:00
Azalea
25edbf06c7 [F] Fix account card linking 2024-04-18 21:47:13 +09:00
Azalea
4f05365da3 [F] Fix results not saved 2024-04-18 12:30:36 +09:00
Azalea
bf7de99524 [+] NFKC Normalization before processing 2024-04-18 11:37:40 +09:00
Azalea
08c27b6c58 [+] Automatically ban people with unacceptable names 2024-04-18 11:30:18 +09:00
Azalea
60661757c6 [+] Safety moderation 2024-04-18 11:29:02 +09:00
Azalea
5ba64483fb [+] OpenAI Settings in application properties 2024-04-18 09:12:34 +09:00
Azalea
a30c9391eb [F] Fix null pointer 2024-04-17 00:06:32 +09:00
Azalea
7023e726bd Merge branch 'v1-dev' of https://github.com/hykilpikonna/AquaDX into v1-dev 2024-04-17 00:03:47 +09:00
Azalea
c616ea81c6 [S] Make b35 look better 2024-04-17 00:03:35 +09:00
Azalea
65f8b587af [S] Fix tooltip startup glitch 2024-04-16 23:48:41 +09:00
Azalea
14f6b9c759 Merge branch 'b50-dev' of https://github.com/colasama/AquaDX into v1-dev 2024-04-16 23:44:30 +09:00
Clansty
c83e0f8cff [F] Fix Chime scanning and (maybe) DX Pass crash 2024-04-12 00:40:29 +08:00
Azalea
2bf86423c9 [PR] #28 from clansty: CustomVersionString
Feature: Custom version display string
2024-04-10 15:36:51 -04:00
凌莞~(=^▽^=)
8dc3035b66 Merge branch 'v1-dev' into feat/CustomVersionString 2024-04-11 03:20:12 +08:00
Azalea
0aff0330e7 [U] Update config description 2024-04-10 07:13:54 -07:00
Azalea
41852f2467 [PR] #29 from clansty: ImproveLoadSpeed
Feature: Option to disable some useless checks and delays to speedup the game boot process
2024-04-10 02:10:35 -04:00
Clansty
442ec76828 [+] Option to disable some useless checks and delays to speedup the game boot process 2024-04-08 17:02:14 +08:00
Clansty
d8fc14e71b [+] Custom version string feature 2024-04-08 15:03:43 +08:00
Azalea
2630d32764 [F] Fix card controller after pdid change 2024-04-07 04:06:17 -04:00
Azalea
74d7eff577 [F] Fix DIVA pd_id overflow 2024-04-06 23:36:00 -04:00
Azalea
355c9e2a3d [+] Counter measure 2024-04-03 08:08:02 -04:00
Azalea
501bf06ada [S] Better mobile alignment 2024-04-02 20:34:20 -04:00
Azalea
4574bc0b2f [F] Fix responsive 2024-04-02 20:25:23 -04:00
Azalea
c6c91b84fe [F] Fix wacca rating calculation 2024-04-02 06:26:18 -04:00
Azalea
066b33e3e8 [F] Fix wacca invalid music 2024-04-02 02:33:32 -04:00
Azalea
15002c45d6 [O] Rewrite chusan item handler 2024-04-02 02:13:20 -04:00
Azalea
b41f3b9370 [F] Fix jvm name clash 2024-04-02 02:07:39 -04:00
Azalea
02e2700e96 [O] Make always vip configurable 2024-04-02 02:04:42 -04:00
Azalea
6441dfd219 [+] Wacca item unlocks 2024-04-02 01:56:42 -04:00
Azalea
1e229c12cc [+] Wacca cheat options 2024-04-02 01:56:31 -04:00
Azalea
4219f2db5b [+] Wacca items 2024-04-02 01:44:12 -04:00
Azalea
36ce636093 [-] Don't display AAA 2024-04-02 01:27:07 -04:00
Azalea
47f09f81ff [U] Correct wacca scoring 2024-04-02 01:26:24 -04:00
Azalea
bfa6df904d [O] Proper ticket unlock for wacca 2024-04-02 00:56:59 -04:00
Azalea
99d4f55c50 [+] Chinese i18n for user settings 2024-04-02 00:28:35 -04:00
Azalea
7728b4b1ab [M] Move setting descriptions to i18n 2024-04-02 00:16:34 -04:00
Azalea
6a475434ad [F] Fix typo 2024-04-02 00:09:14 -04:00
Azalea
876a0bd108 [+] Implement game options 2024-04-02 00:07:50 -04:00
Azalea
ba13bfd9ad [+] SDK for settings/get settings/set 2024-04-01 23:51:13 -04:00
Azalea
44bab8c0c7 [+] Add "type" field to settings/get 2024-04-01 23:51:00 -04:00
Azalea
2d229b82c3 [+] Game settings tab 2024-04-01 23:38:15 -04:00
Azalea
c6cce7aa9a [S] Fix css label box overflow 2024-04-01 23:37:45 -04:00
Azalea
5cbf09f24e [F] Fix splash 2024-04-01 23:27:19 -04:00
Azalea
3ca7d3d615 [U] Update i18n interface 2024-04-01 23:18:50 -04:00
Azalea
25840be694 [U] Update readme jdk version 2024-04-01 19:58:15 -07:00
Azalea
34ab608425 [U] Readme: Add license details 2024-04-01 19:49:50 -07:00
Azalea
b498160b3a [+] CC BY-NC-SA License 2024-04-01 19:37:18 -07:00
Azalea
23aae3b5b9 [F] Fix test build 2024-04-01 22:30:49 -04:00
Azalea
97fdd096a8 [F] Fix music info parsing 2024-04-01 22:19:29 -04:00
Azalea
0d21a02da9 [F] Fix wacca music unlock 2024-04-01 22:17:11 -04:00
Azalea
ab94250b05 [+] Wacca: Unlock all music 2024-04-01 22:12:14 -04:00
Azalea
42ca6f79dc [F] Force color output 2024-04-01 21:59:47 -04:00
Azalea
646795b753 [+] Wacca username character constraint 2024-04-01 21:54:54 -04:00
Azalea
de649915e2 [U] Upgrade to JDK 21 2024-04-01 21:48:08 -04:00
Azalea
0093f5a0de Merge branch 'v1-dev' of https://github.com/hykilpikonna/AquaDX into v1-dev 2024-04-01 21:05:04 -04:00
Azalea
686b50eeda [F] Fix chusan export 2024-04-01 21:04:49 -04:00
Azalea
0c93b85024 [F] Fix docker build
Closes #24
2024-04-01 14:04:19 -04:00
Azalea
49d4e88022 [F] Fix chusan import reflection 2024-03-31 11:53:36 -04:00
Azalea
3a8616e225 [+] Generalize data import for chusan 2024-03-30 23:52:29 -04:00
Azalea
d4178c85a9 [F] Fix wacca song unlock (!) 2024-03-30 20:49:40 -04:00
Azalea
de46790bdf [F] Fix wacca song unlock (?) 2024-03-30 20:31:56 -04:00
Azalea
c27070ae28 [F] Fix ongeki userId overflow 2024-03-30 19:40:47 -04:00
Azalea
bb4c9477da [F] Fix wacca logging 2024-03-29 21:11:28 -04:00
Colanns
95e78e4f93 [+] Add a B50 / rating page to frontend 2024-03-29 23:49:08 +08:00
Azalea
d3d7b5a5c7 [O] Don't translate game name 2024-03-29 09:15:48 -04:00
Azalea
45a3d74284 [F] Fix wacca query 2024-03-29 09:02:30 -04:00
Azalea
cd972b5c61 [+] Add wacca to frontend 2024-03-29 08:20:37 -04:00
Azalea
341be8bdc1 [+] Add wacca to card controller 2024-03-29 08:20:21 -04:00
Azalea
101c24edc5 [+] Add wacca support to readme 2024-03-29 05:58:27 -04:00
Azalea
be34915cdf [+] Wacca: Calculate player rating server side 2024-03-29 05:53:00 -04:00
Azalea
70aed1d5db [+] Wacca endpoints 2024-03-29 05:45:41 -04:00
Azalea
d8c1144881 [-] Remove spring devtools 2024-03-29 05:13:43 -04:00
Azalea
68ec7f504a [+] Wacca NET controller (incomplete) 2024-03-29 05:13:22 -04:00
Azalea
3ab2b16042 [+] Chusan 216 events 2024-03-29 05:12:54 -04:00
Azalea
d7fc6f9f49 [F] Fix music difficulty unlock conflict 2024-03-29 02:58:25 -04:00
Azalea
26a72244c0 [M] Rename database fields 2024-03-29 00:47:54 -04:00
Azalea
abc21badb1 [+] Wacca: More database fields 2024-03-29 00:24:02 -04:00
Azalea
aa1caacfd6 [F] Fix AllNet Wacca path 2024-03-29 00:09:40 -04:00
Azalea
3663eb63e7 [F] Fix maimai2 UpsertUserChargelogApi (TODO) 2024-03-29 00:09:27 -04:00
Azalea
e885700680 [+] wacca user/goods/purchase 2024-03-28 23:34:38 -04:00
Azalea
d6170d602a [+] wacca user/trial/update 2024-03-28 23:34:26 -04:00
Azalea
4dbb287e11 [-] Remove version 2024-03-28 23:34:05 -04:00
Azalea
e537e0f115 [U] Update migration 2024-03-28 23:19:51 -04:00
Azalea
3613d7a37b [+] user/trial/get, user/vip/get, user/vip/start 2024-03-28 23:19:05 -04:00
Azalea
b5e98f505f [O] Code cleanup 2024-03-28 22:45:34 -04:00
Azalea
3dc9ca6822 [+] Wacca api progress 2024-03-28 22:36:50 -04:00
Azalea
e13ddeaaad [+] user/music/unlock 2024-03-28 22:07:03 -04:00
Azalea
56ce7f9696 [F] Wacca fix impl details 2024-03-28 20:17:33 -04:00
Azalea
4ebddf78ed [F] Wacca: Fix handle 2024-03-28 19:08:59 -04:00
Azalea
2682165da8 [+] Wacca: user/info/update 2024-03-28 19:05:54 -04:00
Azalea
373e7dc8ad [+] Wacca: user/status/update 2024-03-28 19:05:46 -04:00
Azalea
0551f8bff1 [+] Wacca: user/rating/update 2024-03-28 19:05:35 -04:00
Azalea
b4454cc812 [+] Wacca: Fix test inconsistency, error handling 2024-03-28 19:05:19 -04:00
Azalea
40fb1c8868 [O] Wacca: Simplify data storage, re-init database 2024-03-28 19:04:16 -04:00
Azalea
f97cb4a1bb [F] Fix integer list converter behavior on empty lists 2024-03-28 19:02:21 -04:00
Azalea
56d0786702 [+] Wacca more tests 2024-03-28 11:09:29 -04:00
Azalea
d880ecd709 [+] Wacca user/music/update 2024-03-28 11:09:12 -04:00
Azalea
1bee9e19e6 [+] Wacca user/mission/update 2024-03-28 11:09:01 -04:00
Azalea
c5879ae5a7 [+] Wacca user/sugoroku/update 2024-03-28 11:08:48 -04:00
Azalea
64f458e15a [F] Fix consecutive login 2024-03-28 11:08:33 -04:00
Azalea
2fa5d09fc9 [O] Redesign wacca score model 2024-03-28 11:08:09 -04:00
Azalea
d6fc60e02b [F] Wacca user/status/getDetail 2024-03-28 07:05:32 -04:00
Azalea
bb9bfd6396 [+] Wacca user/status/GetDetail 2024-03-28 05:57:07 -04:00
Azalea
0fbe139e8d [F] Fix wacca db constraints 2024-03-28 05:56:52 -04:00
Azalea
571591f021 [O] Unify item interface 2024-03-28 05:24:05 -04:00
Azalea
8a1d2383b8 [+] Wacca user/status/login tests 2024-03-28 03:37:26 -04:00
Azalea
00c5edcea7 [+] Wacca user/status/login 2024-03-28 03:37:14 -04:00
Azalea
39d62099df [F] Fix status return 2024-03-28 02:51:41 -04:00
Azalea
c5d6f6f5b9 [+] Wacca user/status/create 2024-03-28 02:50:44 -04:00
Azalea
13f3cf1e90 [+] Test constants 2024-03-28 02:10:39 -04:00
Azalea
93f6bf8ba3 [+] Wacca user/status/get 2024-03-28 02:10:25 -04:00
Azalea
1cdbed51cd [F] Fix long casting 2024-03-28 01:56:23 -04:00
Azalea
50ae04bb4e [+] Wacca test (incomplete) 2024-03-28 01:22:16 -04:00
Azalea
a55d503faa [F] Fix allnet compression 2024-03-28 01:14:29 -04:00
Azalea
7fc4f83eb5 [F] Fix test build 2024-03-28 01:05:13 -04:00
Azalea
bc831b4d30 [F] Fix filter 2024-03-28 01:00:59 -04:00
Azalea
c6190146aa [F] Fix zlib compression happening after response commit 2024-03-28 00:58:55 -04:00
Azalea
3f01152a4a [+] More extensions 2024-03-27 23:11:05 -04:00
Azalea
ad5c652a8f [+] Return wacca url for AllNet 2024-03-27 23:10:52 -04:00
Azalea
9609db941b [+] Wacca server handler (incomplete) 2024-03-27 23:10:41 -04:00
Azalea
bbb8447f5c [+] Wacca constants & repos 2024-03-27 23:09:13 -04:00
Azalea
22ca06af3e [+] Wacca request model 2024-03-27 23:08:58 -04:00
Azalea
af11758190 [+] Exclude wacca in compression filter 2024-03-27 23:08:22 -04:00
Azalea
32fcc25ea4 [F] mai2 error response 2024-03-27 23:07:27 -04:00
Azalea
b3fcf8dd5e [F] Fix mai2 error response 2024-03-27 23:05:46 -04:00
Azalea
b7d2a97f05 [O] Separate register function 2024-03-27 22:42:05 -04:00
Azalea
ad13875137 [O] Separate common functions for tests 2024-03-27 22:39:27 -04:00
Azalea
e14a131480 [F] Fix wacca db migration for MariaDB 11.3.2 2024-03-27 04:48:04 -04:00
Azalea
64ba0db228 [F] Fix memory leak 2024-03-27 00:52:21 -04:00
Azalea
c99d8e7e75 [O] More cleanup, return 400 for bad requests 2024-03-26 23:03:40 -04:00
Azalea
305d1cea94 [O] Disable integration tests on build 2024-03-26 22:31:49 -04:00
Azalea
f314b3982e [F] Fix mai event id 2024-03-26 22:27:12 -04:00
Azalea
5ea2615b93 [O] Collapse basic handlers 2024-03-26 22:26:29 -04:00
Azalea
17123fec35 [F] Remove redundant fields in GetUserMap, LoginBonus, UserExtend, UserData 2024-03-26 20:52:40 -04:00
Azalea
73d05e7cbf [F] Fix bearer discrepency 2024-03-26 20:51:10 -04:00
Azalea
3380ea3609 [F] Fix mai2 username encoding check 2024-03-26 20:50:57 -04:00
Azalea
101527d3e1 [U] Update testing properties 2024-03-26 20:50:30 -04:00
Azalea
df9ab3250c [+] Maimai2 play simulation testing 2024-03-26 20:49:40 -04:00
Azalea
d533df52de [F] Fix maimai2 user item 2024-03-26 20:22:38 -04:00
Azalea
d2cf16d046 [F] Fix username decoding 2024-03-26 18:51:17 -04:00
Azalea
40a65b5e13 [+] gzip & deflate 2024-03-26 18:09:27 -04:00
Azalea
fa33cb680e [PR] #23 from Teud/v1-dev
Add maimai 1.40 H061 events
2024-03-25 22:50:48 -04:00
Teud
2757eb91ce fix 2024-03-25 23:12:00 +01:00
Teud
2842429ced Add maimai 1.40 H061 events 2024-03-25 23:04:16 +01:00
Azalea
fb2a26c5b7 [F] Fix dependencies 2024-03-25 14:21:14 -04:00
Azalea
cab1dc8838 [O] Set all items to valid 2024-03-25 14:18:04 -04:00
Azalea
0ec76dcde3 [F] JsonIgnore ID 2024-03-25 14:15:49 -04:00
Azalea
c41046953e [F] Fix user item isValid field 2024-03-25 14:15:03 -04:00
Azalea
30f740a430 [-] Remove old code 2024-03-25 13:56:49 -04:00
Azalea
1e8c0ce99b [O] Optimize mai2 GetUserMusic 2024-03-25 13:56:30 -04:00
Azalea
aa3a3d9181 [F] Fix chusan playlog integer overflow 2024-03-25 13:40:41 -04:00
Azalea
6d0f528201 [F] Fix ongeki upsert all: UserData might be empty list 2024-03-25 13:39:00 -04:00
Azalea
131cd5915c [F] Fix chusan user cmission saving bug 2024-03-25 13:29:49 -04:00
Azalea
f5512fa162 [F] Fix db: Make user_id non-null 2024-03-25 03:07:28 -04:00
Azalea
484bb758ae [+] Wacca database models 2024-03-25 03:06:28 -04:00
Azalea
89461893a4 [+] Allow ftk as an auth token 2024-03-23 12:22:32 -04:00
Azalea
54e865feb2 [O] Optimize GetUserItemApi loading speed 2024-03-23 07:19:58 -04:00
Azalea
015fa3dc9f [F] Fix maimai2 events 2024-03-23 07:01:00 -04:00
Azalea
cf015be49f [F] Fix maimai get rating crash for some users 2024-03-23 05:53:56 -04:00
Azalea
b6c8993f7e [F] Fix total achievement overflowing int32 max 2024-03-23 02:24:10 -04:00
Azalea
1ef37d91e8 Merge branch 'v1-dev' of https://github.com/hykilpikonna/AquaDX into v1-dev 2024-03-22 16:59:47 -04:00
Azalea
7fc81cf363 [+] Add chusan 220 events 2024-03-22 16:59:42 -04:00
Azalea
123bf9de34 [U] Update README.md 2024-03-22 13:11:06 -07:00
Azalea
d3f6b75d34 [F] Fix reflection 2024-03-22 15:17:27 -04:00
Azalea
a5fe5f53e2 [+] chusan: Luminous 2024-03-22 15:09:29 -04:00
Azalea
e91029f66e [F] Fix merge conflicts 2024-03-22 15:06:31 -04:00
Azalea
2a7ce54c28 Merge dev.s-ul.net:rinsama/aqua into v1-dev 2024-03-22 14:58:41 -04:00
HoshimiRIN
f3b2d4dc57 [chusan]fix build issue 2024-03-22 14:51:52 +08:00
HoshimiRIN
95b9871f7f [chusan]add support for luminous 2024-03-22 14:29:43 +08:00
Azalea
533af83749 [O] Don't jsonignore id 2024-03-21 22:28:42 -04:00
Azalea
e4330fee92 [F] Fix artemis conversion 2024-03-21 04:09:48 -04:00
Azalea
5fec57e8e3 [+] Add artemis import endpoint 2024-03-21 04:04:23 -04:00
Azalea
95a06d572b [F] Fix mai2 import 2024-03-21 03:59:35 -04:00
Azalea
cc8c125934 [F] Fix deleting data on import 2024-03-21 01:07:25 -04:00
Azalea
91c605ee4b [F] Fix unique constraints on user detail 2024-03-21 01:00:52 -04:00
Azalea
f44fe4def1 [+] Mai2 Import feature done! 2024-03-21 00:36:23 -04:00
Azalea
7c0a1ea089 [F] Fix mai2 entity nullability 2024-03-21 00:36:00 -04:00
Azalea
ce5c4d1111 [F] Fix userid nullability 2024-03-21 00:35:57 -04:00
Azalea
98952972a0 [+] Add cascade relationship to user-mapped fks 2024-03-21 00:05:58 -04:00
Azalea
f728b6ab1b [O] Check username 2024-03-20 22:43:03 -04:00
Azalea
e799b48877 [O] Huge refactor 2024-03-20 21:27:29 -04:00
Azalea
fc8ecb7470 [+] More work on import feature (TODO) 2024-03-20 18:52:11 -04:00
Azalea
ac18234e29 [+] Spring devtools dependency 2024-03-20 17:57:34 -04:00
Azalea
59b17aa47e [+] Check sqlite before application start 2024-03-20 17:54:16 -04:00
Azalea
9155bfb886 [+] SDGS support 2024-03-20 13:01:57 -04:00
Azalea
cbe683d25e [F] Fix matching server 2024-03-20 12:57:01 -04:00
Azalea
64f057a415 [F] Fix build 2024-03-20 12:46:08 -04:00
Azalea
3da308346e [O] Unify even more BaseHandlers 2024-03-20 12:45:46 -04:00
Azalea
313dd681de [+] Import (TODO) 2024-03-20 12:33:33 -04:00
Azalea
aaf7e1e3e5 [+] Chusan export version support 2024-03-20 12:32:34 -04:00
Azalea
9f831fd8b5 [O] Refactor chusan controller 2024-03-20 12:32:14 -04:00
Azalea
450397481e [M] Unify BaseHandler 2024-03-20 12:24:31 -04:00
Azalea
6fb8978f48 [+] Chusan playlog sunplus fields 2024-03-20 05:13:47 -04:00
Azalea
4a7bf4b31e [+] Chusan artemis import 2024-03-20 05:09:42 -04:00
Azalea
38e94210e4 [+] Jackson datetime parsing 2024-03-20 05:08:57 -04:00
Azalea
d338809750 [M] Generalize artemis import code 2024-03-20 04:17:35 -04:00
Azalea
7fd7e17d1d [+] mai2 artemis import script 2024-03-20 02:00:28 -04:00
Azalea
a5a5bd80c4 [+] Helper to read sql file 2024-03-20 01:59:21 -04:00
Azalea
d264ca1ed4 [+] Custom json deserializers 2024-03-20 01:59:08 -04:00
Azalea
85dd19509c [M] Move class 2024-03-19 21:04:07 -04:00
Azalea
faf1945933 [M] Move packages 2024-03-19 21:02:24 -04:00
Azalea
3c6d6ff702 [-] Ignore log files 2024-03-19 20:52:20 -04:00
Azalea
c6ecc89ad3 [+] user-detail endpoint 2024-03-19 20:52:04 -04:00
Azalea
abed79441d [U] Upgrade to kotlin 2.0.0-Beta4 2024-03-19 20:22:28 -04:00
Azalea
906199a517 [+] Change in-game settings 2024-03-19 19:58:00 -04:00
Azalea
6f34c21d94 [O] Optimize startup speed 2024-03-18 18:56:44 -04:00
Azalea
9ba1a68b51 [F] Fix setting api 2024-03-18 18:56:16 -04:00
Azalea
073c72fd63 [F] Fix error response 2024-03-18 09:34:37 -04:00
Azalea
3ac4af1558 [F] Fix typo 2024-03-18 09:33:33 -04:00
Azalea
5057f6848f [+] Settings api 2024-03-18 09:32:14 -04:00
Azalea
b3955731c2 [+] Maimai item unlock 2024-03-18 05:31:55 -04:00
Azalea
af83cf552e [S] Coming soon message 2024-03-18 04:03:26 -04:00
Azalea
a0426044e8 [+] i18n for home page 2024-03-18 03:57:58 -04:00
Azalea
432635d567 [F] Fix entrypoint 2024-03-18 03:06:27 -04:00
Azalea
02b78320ec [+] Better logging 2024-03-18 03:06:05 -04:00
Azalea
f1461f905d [+] i18n for status messages 2024-03-18 01:55:22 -04:00
Azalea
e1cdb3ab65 [F] More detailed error handling 2024-03-18 01:37:57 -04:00
Azalea
6218424be3 [O] Better error handling 2024-03-18 01:32:57 -04:00
Azalea
5a9b7e296f [O] Reduce loc 2024-03-18 01:32:38 -04:00
Azalea
f4cc9c7734 [F] Fix maimai music unlock 2024-03-18 01:32:00 -04:00
Azalea
e0c7998448 [+] DB cleanup 2024-03-18 01:31:39 -04:00
Azalea
752d65557f [+] Unlock option database model 2024-03-18 01:31:28 -04:00
Azalea
a952674df7 [+] Maimai music unlock 2024-03-17 02:03:58 -04:00
Azalea
25f5f6e1f7 [+] Maimai export 2024-03-17 00:12:02 -04:00
Azalea
0f1d6c0984 [O] Remove unnecessarily long constructors 2024-03-16 22:50:08 -04:00
Azalea
8dd4bb9d61 [M] Rename 2024-03-16 22:14:18 -04:00
Azalea
98275ade59 [O] Refactor chusan repos 2024-03-16 22:09:21 -04:00
Azalea
95cc9f0e21 [M] Rename 2024-03-16 21:41:17 -04:00
Azalea
742ea50c2c [O] More simplification 2024-03-16 21:35:17 -04:00
Azalea
54b1174e1b [O] Refactor maimai2 repositories 2024-03-16 19:50:05 -04:00
Azalea
e07de72fa4 [O] Generalize ranking and find by card 2024-03-16 19:22:08 -04:00
Azalea
13b4af3734 [M] Move files 2024-03-16 19:10:22 -04:00
Azalea
29566a6c93 [O] Make unclickable when no data is available 2024-03-15 01:33:07 -04:00
Azalea
7669f7d9a0 [O] Refactor 2024-03-15 01:26:52 -04:00
Azalea
5913d5b585 [F] Fix script 2024-03-15 01:13:18 -04:00
Azalea
d9a332de44 [+] Display rating details 2024-03-15 01:10:15 -04:00
Azalea
e85533686e [F] Fix detailed ranks 2024-03-15 00:37:30 -04:00
Azalea
0100140dc0 Revert "[+] Detailed ranks"
This reverts commit a9893379f4.
2024-03-14 23:21:30 -04:00
Azalea
8def9e8931 [F] Fix link 2024-03-14 22:59:26 -04:00
Azalea
6fc2f26983 [O] Optimize fonts 2024-03-14 22:55:11 -04:00
Azalea
ed1ed6cbe9 [+] Leaderboard i18n 2024-03-14 22:41:23 -04:00
Azalea
10d19a5392 [+] Switching games in leaderboard 2024-03-14 22:35:36 -04:00
Azalea
7bbd90ab91 [+] Emphasize registered users 2024-03-14 22:23:48 -04:00
Azalea
9565d48b04 [F] Fix "no-data" 2024-03-14 22:15:19 -04:00
Azalea
284d366b44 [+] Ranking for different games 2024-03-14 22:15:10 -04:00
Azalea
a9893379f4 [+] Detailed ranks 2024-03-14 22:11:04 -04:00
Azalea
50677ad81d [F] Fix no data when a user hasn't played maimai 2024-03-14 21:47:39 -04:00
Azalea
71d7fcbe65 [+] Get user games endpoint 2024-03-14 21:47:14 -04:00
Azalea
8342acbd49 [F] Fix heatmap displaying even when plays is 0 2024-03-06 17:04:51 -05:00
Azalea
d5296763ad [O] Do not overwrite println in gendocs 2024-03-06 10:49:02 -05:00
Azalea
73efa4fe91 [U] Update docs 2024-03-06 10:46:11 -05:00
Azalea
82f573e1a1 [O] More information to frontier endpoint 2024-03-06 10:42:31 -05:00
Azalea
4ef0ac3fee [F] Fix typo 2024-03-06 10:37:06 -05:00
Azalea
bc246f39d2 [+] Frontier endpoint 2024-03-06 10:34:12 -05:00
Azalea
f9af23dbca [+] More i18n 2024-03-06 10:33:31 -05:00
Azalea
68f8ef0b24 [O] More i18n 2024-03-06 10:17:04 -05:00
Azalea
16f6acf8fc [+] More i18n 2024-03-06 10:13:08 -05:00
Azalea
3faa5b2f52 [F] Fix typo 2024-03-06 09:57:05 -05:00
Azalea
04a7c068f4 [M] Fix typos 2024-03-06 09:52:37 -05:00
Azalea
92dee27634 [F] Fix i18n typing 2024-03-06 09:51:03 -05:00
Azalea
7dda25f96b Merge pull request #22 from chiba233/v1-dev
[U] update i18n.ts type and more intuitive
2024-03-06 09:32:49 -05:00
chiba
40f700910a Update UseHome.svelte I18n support 2024-03-06 21:18:27 +08:00
chiba
aa90b34511 [F] FIX i18n.ts type 2024-03-06 17:19:51 +08:00
chiba
45cf082bb9 [F] FIX i18n.ts type 2024-03-06 16:23:45 +08:00
chiba
0ab78983d4 [U] update i18n.ts type and more intuitive 2024-03-06 16:17:06 +08:00
Azalea
e137210cbc [S] Better title 2024-03-06 01:33:47 -05:00
Azalea
3093755c9e [S] Better heading style 2024-03-06 01:31:16 -05:00
Azalea
94c4950d23 [O] i18n placeholders 2024-03-06 01:25:08 -05:00
Azalea
fa0a624b7c [O] Infer language 2024-03-06 01:21:11 -05:00
Azalea
f3fabe1708 [+] i18n 2024-03-06 01:20:28 -05:00
Azalea
52ec890e2c [+] Show aqua net pfp instead of in-game pfp 2024-03-06 00:37:35 -05:00
Azalea
2a10471e0b [O] Don't expose all fields 2024-03-06 00:35:44 -05:00
Azalea
94c1974d2f [O] Just pass through aquanetuser instead. 2024-03-06 00:26:34 -05:00
Azalea
f0a8014efb [+] Add profile picture in game summary 2024-03-06 00:25:02 -05:00
Azalea
96cac6ca68 [O] Optimize imports 2024-03-06 00:21:27 -05:00
Azalea
0da50bc693 [F] Fix profile path 2024-03-06 00:20:04 -05:00
Azalea
1169ac44b4 [-] Remove path concat 2024-03-06 00:18:28 -05:00
Azalea
38367279ff [F] Fix upload paths 2024-03-06 00:16:53 -05:00
Azalea
ef00cfbddd [+] Serve uploads directory 2024-03-06 00:07:57 -05:00
Azalea
e514e4b64e [+] Wrapper for pfp path 2024-03-05 23:35:49 -05:00
Azalea
f1af07e921 [+] UI for uploading profile picture 2024-03-05 23:27:08 -05:00
Azalea
44cf022e70 [F] Fix profile file name extension 2024-03-05 23:26:11 -05:00
Azalea
7e68de5a17 [F] Fix profile picture upload api 2024-03-05 23:24:41 -05:00
Azalea
51f73d77bf [F] Fix felica lookup v2 2024-03-05 22:34:53 -05:00
Azalea
fa4ccf07b8 [+] Implement user settings 2024-03-05 22:18:38 -05:00
Azalea
49da7aafd0 [O] Show edit profile button only if it's me 2024-03-05 19:36:23 -05:00
Azalea
58ca71baaa [+] Tooltip 2024-03-05 19:32:36 -05:00
Azalea
2c550a0874 [S] Unify border radius 2024-03-05 19:19:05 -05:00
Azalea
dcb671acd8 [+] Profile setting button in home 2024-03-05 19:09:43 -05:00
Azalea
56600d3f27 [O] Change default hosts 2024-03-05 18:22:20 -05:00
Azalea
6913f7bdf5 [F] Fix types
#21
2024-03-05 18:21:32 -05:00
Azalea
bcc2d286ed [+] Display profile picture
#21
2024-03-05 18:21:05 -05:00
Azalea
aed6c2123f [F] Fix type nullability 2024-03-05 18:20:03 -05:00
Azalea
68626fecd7 [+] Add profile picture field to aqua net user 2024-03-05 17:59:44 -05:00
Azalea
441d7376cb [+] Upload pfp endpoint 2024-03-05 17:56:33 -05:00
Azalea
c9ac38de01 [+] Optimize upload photo 2024-03-05 17:56:16 -05:00
Azalea
b9c063c41e [O] Reject unauthenticated aimedb requests 2024-03-05 14:47:02 -05:00
Azalea
55804be70e [F] Fix code suggestions
Closes #20
2024-03-05 14:25:11 -05:00
Azalea
2b749af917 [+] Filter dates on frontend 2024-03-05 03:44:16 -05:00
Azalea
9378dfdd04 [F] Fix graph logic 2024-03-05 03:41:23 -05:00
Azalea
46768c77b7 [S] Hide heatmap scrollbar and automatically scroll to right 2024-03-05 03:32:55 -05:00
Azalea
ff9358b986 [F] Fix index out of bounds 2024-03-05 03:22:53 -05:00
Azalea
f3090870be [O] Don't filter on backend 2024-03-05 03:22:07 -05:00
Azalea
666fbe8ce7 [+] Display no data 2024-03-05 03:21:49 -05:00
Azalea
67b29851ea [F] Fix utage song info 2024-03-05 02:53:04 -05:00
Azalea
1a2cd201a7 Merge pull request #18 from chiba233/v1-dev
improve UserHome.svelte song level and music name
2024-03-05 02:51:40 -05:00
Azalea
5041bf67a5 Merge branch 'v1-dev' into v1-dev 2024-03-05 02:51:33 -05:00
Azalea
1a2f3bf80e [F] Fix user page css text overflow 2024-03-05 02:50:00 -05:00
chiba
d7a231eb18 improve UserHome.svelte song level and music name 2024-03-05 15:41:01 +08:00
Azalea
21c9c190aa Merge branch 'v1-dev' of https://github.com/hykilpikonna/AquaDX into v1-dev 2024-03-04 21:18:26 -05:00
Azalea
a781c2d665 [-] Delete unused files 2024-03-04 21:18:17 -05:00
Azalea
09c3ce3164 [PR] #17 from chiba233/v1-dev
improve LinkCard.svelte more intuitive
2024-03-04 21:08:16 -05:00
Azalea
a7888a63fa [F] Fix typing 2024-03-04 21:05:32 -05:00
chiba
79dd56d017 improve LinkCard.svelte when click button clean input 2024-03-04 23:37:56 +08:00
chiba
882d04f50c add .editorconfig to improve multiple developers working 2024-03-04 23:30:47 +08:00
chiba
a7fd414ce6 improve LinkCard.svelte more intuitive 2024-03-04 22:26:48 +08:00
Azalea
eb30451cfa [O] Aimedb: ignore invalid requests 2024-03-03 18:01:27 -05:00
Azalea
a48f2b1f17 [-] Remove jsonignore for playlog id 2024-03-03 17:48:13 -05:00
Azalea
d3665d64a6 [+] Default game endpoint 2024-03-03 17:46:40 -05:00
Azalea
a4bbc9c3c6 [+] Recent endpoint 2024-03-03 17:46:22 -05:00
Azalea
b0ffda42bc [+] User profile game switching 2024-03-03 17:10:13 -05:00
Azalea
b333045d41 [F] Fix out of bounds 2024-03-03 17:09:55 -05:00
Azalea
ef5d0a81eb [O] Display relative time in rating chart 2024-03-03 15:31:59 -05:00
Azalea
48819c10a9 [O] Better trend graph 2024-03-03 14:53:18 -05:00
Azalea
9ae23e4395 [F] Fix import 2024-03-03 14:46:55 -05:00
Azalea
4d36efebb7 [U] Update app version 2024-03-03 14:45:24 -05:00
Azalea
e842a37654 [+] Display splash and app version in frontend 2024-03-03 14:45:15 -05:00
Azalea
c821626dc1 [+] Remake self test 2024-03-03 14:32:21 -05:00
Azalea
16aba9ff96 [O] Render gradient splash text using HyLogger 2024-03-03 14:26:27 -05:00
Azalea
a11bfdb13b [F] Reset turnstile after login error 2024-03-03 12:30:52 -05:00
Azalea
c0437e55eb [F] Fix token invalidation 2024-03-03 12:24:20 -05:00
Azalea
279bcbfeab [+] More instructions 2024-03-03 02:00:32 -05:00
Azalea
6555263496 [F] Fix discord page link 2024-03-03 01:05:23 -05:00
Azalea
8db4e17a8a [F] Fix login redirect logic 2024-03-03 01:00:36 -05:00
Azalea
4a5bd3135f [O] More logging 2024-03-03 00:47:57 -05:00
Azalea
32eb98361a [+] Redirect option 2024-03-03 00:29:48 -05:00
Azalea
2ba5073d55 [M] Rename project & prepare for 1.0.0 2024-03-03 00:00:33 -05:00
Azalea
9ea5e2cd90 [-] Disable plain jar 2024-03-02 23:59:03 -05:00
Azalea
284a1f0b57 [S] Better hover transition 2024-03-02 23:32:32 -05:00
Azalea
7b97f3d535 [S] Blur for loading 2024-03-02 23:19:25 -05:00
Azalea
fb431fcc7b [+] Add discord link in setup instructions 2024-03-02 23:15:28 -05:00
Azalea
b16100e627 [S] Minor text inconsistency 2024-03-02 23:08:50 -05:00
Azalea
fde6b5df9b [S] Minor adjustments 2024-03-02 23:07:01 -05:00
Azalea
dc1ac106c0 [-] Remove unused code 2024-03-02 23:04:29 -05:00
Azalea
0d4a26c05e [+] Action card 2024-03-02 23:03:55 -05:00
Azalea
ffe7a9294b [+] Home action cards 2024-03-02 23:03:46 -05:00
Azalea
15004b6ba2 [S] Better home page tab styling 2024-03-02 20:22:11 -05:00
Azalea
dfd8d1b0c9 [-] Remove the unnecessary clz shorthand 2024-03-02 19:55:52 -05:00
Azalea
c2fef3fa25 [+] Home page tabs 2024-03-02 19:48:29 -05:00
Azalea
d33c892303 [+] Better fading 2024-03-02 19:47:45 -05:00
Azalea
e4ce97cf5d [+] Add UI constants 2024-03-02 19:45:08 -05:00
Azalea
f331916bd5 [O] Use better error and loading 2024-03-02 19:32:16 -05:00
Azalea
5d3194dd41 [O] Better loading and error display 2024-03-02 19:29:35 -05:00
Azalea
f2574b516e [F] Fix: Automatically redirect users if not logged in 2024-03-02 19:27:13 -05:00
Azalea
9ee3e973c1 [+] Add discord invite url 2024-03-02 19:26:55 -05:00
Azalea
eb9e797017 Merge branch 'v1-dev' of https://github.com/hykilpikonna/AquaDX into v1-dev 2024-03-02 11:53:21 -05:00
Azalea
d0c305b3eb [+] Playlog api 2024-03-02 11:53:19 -05:00
Galexion
db2a7208da AquaNet: got typescript to stop yelling at me, it was driving me up a wall 2024-03-01 23:16:14 -05:00
Galexion
e8958f5e53 AquaNet: Added Loading Circle 2024-03-01 01:37:59 -05:00
Azalea
8acee1251f [+] Return card id in ranking 2024-03-01 00:45:57 -05:00
Azalea
acf117e43b [O] Use user{id} instead 2024-03-01 00:42:39 -05:00
Azalea
ed1b7f477b [F] Not extId 2024-03-01 00:40:57 -05:00
Azalea
63cf1f5fa1 [+] Allow querying card user ids 2024-03-01 00:38:33 -05:00
Azalea
fa9b738cba Merge branch 'v1-dev' of https://github.com/hykilpikonna/AquaDX into v1-dev 2024-03-01 00:30:38 -05:00
Azalea
3efbefe4c5 [+] Limit user creation 2024-03-01 00:30:37 -05:00
Galexion
dcb797db38 AquaNet: Make Registered users on Ranking Page link back to their user profile 2024-03-01 00:16:58 -05:00
Azalea
8f9f9e9e82 [F] Fix code overflow on mobile 2024-02-29 23:59:49 -05:00
Azalea
642754a46b Merge branch 'v1-dev' of https://github.com/hykilpikonna/AquaDX into v1-dev 2024-02-29 23:57:52 -05:00
Galexion
a5578335d3 AquaNet: Append to make Error Message h2 not use margin-top 2024-02-29 22:50:58 -05:00
Galexion
c4309aa14c AquaNet: Added an Error Message trigger to pages that didn't telegraph to the user that there was an error before. 2024-02-29 22:48:41 -05:00
Azalea
af3eb10034 [+] Cache ranking 2024-02-29 19:31:38 -05:00
Azalea
88b7804123 [O] Simplify userlogin 2024-02-29 18:31:05 -05:00
Azalea
4a383521d7 [+] Properly update last access time for card 2024-02-29 18:30:20 -05:00
Azalea
279b65cfa0 [+] Return username in ranking 2024-02-29 18:29:37 -05:00
Azalea
6ce644ea18 [F] Fix NPE when no play data is found 2024-02-29 16:57:23 -05:00
Azalea
9ef5e8d037 [F] Fix user id being null 2024-02-29 16:53:02 -05:00
Azalea
a9e14a93dd [F] Fix mai2 playlog by introducing a backlog 2024-02-29 16:24:56 -05:00
Azalea
8e2c0d8653 [F] Fix transition 2024-02-29 11:21:29 -05:00
Azalea
39a19fd9e6 [F] Fix accuracy calculation 2024-02-29 11:14:13 -05:00
Azalea
68e1a0489f [S] Style ranking 2024-02-29 11:07:44 -05:00
Azalea
ece64c3f4a [F] Fix ranking typing 2024-02-29 10:42:22 -05:00
Azalea
37f67469a6 [F] Fix keychip flow
q
2024-02-29 10:38:12 -05:00
Azalea
a1b546152b [-] Remove unnecessary tests 2024-02-29 10:30:48 -05:00
Azalea
3ae1f6c556 Merge branch 'v1-dev' of https://github.com/hykilpikonna/AquaDX into v1-dev 2024-02-29 10:30:00 -05:00
Azalea
14757e2a35 [F] Fix gradle build 2024-02-29 10:29:55 -05:00
Galexion
d20a762dd8 AquaNet Ranking Page: Dived and classed instead 2024-02-29 01:05:07 -05:00
Galexion
5ff79f5ee1 Leaderboard Rough Draft 2024-02-29 00:59:05 -05:00
Azalea
afe28733db [+] Setup instructions: Segatools configruation 2024-02-29 00:50:29 -05:00
Azalea
5e5fe6013d [+] Install shiki dependnecy 2024-02-29 00:49:53 -05:00
Azalea
3f0196c8f8 [+] Keychip sdk 2024-02-29 00:49:24 -05:00
Azalea
4e38cf9d40 [+] Keychip endpoint 2024-02-29 00:48:55 -05:00
Azalea
6026f6aebd [+] Export aqua connection host 2024-02-29 00:25:57 -05:00
Azalea
856bcf1647 [+] Aqua connection host in config 2024-02-29 00:25:06 -05:00
Azalea
8e7196181c [F] Fix accuracy 2024-02-29 00:08:48 -05:00
Azalea
beb6697507 [F] Fix all perfect count 2024-02-28 23:37:27 -05:00
Azalea
fd482d32a7 [+] Ranking endpoint 2024-02-28 22:09:53 -05:00
Azalea
10169b03ce [+] API Doc generator 2024-02-28 20:52:23 -05:00
Azalea
5f4a7cd7c9 [+] API documentation 2024-02-28 20:45:38 -05:00
Azalea
59b52b8a47 [F] Fix delete transaction 2024-02-28 20:08:45 -05:00
Azalea
02bffab38f [F] Fix #16 2024-02-28 20:07:31 -05:00
Azalea
256f08396f [+] Setup instrcutions page 2024-02-28 17:55:52 -05:00
Azalea
988a280111 [S] Blockquote styling 2024-02-28 17:55:44 -05:00
Azalea
7a44a457d5 [F] Fix another DIVA endpoint 2024-02-28 17:21:08 -05:00
Azalea
2fa153e569 [F] Fix diva request mapping 2024-02-27 23:47:05 -05:00
Azalea
b589c78cfc [+] More logging for Diva compression 2024-02-27 23:16:45 -05:00
Azalea
293acbcc03 [O] Hide unnecessarily long EOF error for Diva compression 2024-02-27 22:55:43 -05:00
Azalea
be0a841926 [F] Fix downloadOrder typo 2024-02-27 22:52:31 -05:00
Azalea
763cbfa656 [F] Fix full combo count 2024-02-27 22:46:49 -05:00
Azalea
d149b02c06 Merge pull request #14 from Sensokaku/patch-1
Fix playtime
2024-02-27 19:38:21 -05:00
Sensokaku
b83773dfa6 Fix playtime
Playtime wasn't being showing up correctly till I removed one of the 60's
2024-02-28 05:36:39 +08:00
Azalea
c992701387 [S] Fix level color 2024-02-27 13:34:06 -05:00
Azalea
bf43944c27 [S] Fix level color 2024-02-27 13:33:41 -05:00
Azalea
6dbed875e1 [F] Fix interface treated as repository 2024-02-27 13:21:16 -05:00
Azalea
5166387f34 [O] Generalize card migration 2024-02-27 13:17:27 -05:00
Azalea
b44121597f [F] Fix apis 2024-02-26 23:19:10 -05:00
Azalea
f086b8abe9 [+] Image on error 2024-02-26 23:18:03 -05:00
Azalea
795da9557b [O] Better url scheme 2024-02-26 20:17:39 -05:00
Azalea
76249cb8f7 [+] Chunithm adaptation 2024-02-26 20:17:30 -05:00
Azalea
f4c4162e4b [+] Add game api in sdk 2024-02-26 20:17:06 -05:00
Azalea
902cc9009e [M] Move scoring to separate file 2024-02-26 20:16:49 -05:00
Azalea
d93c2ee267 [+] More types 2024-02-26 20:16:31 -05:00
Azalea
e5b864f07e [+] No profile profile image 2024-02-26 20:16:17 -05:00
Azalea
8df4cd3dd6 [+] No cover cover image 2024-02-26 20:16:10 -05:00
Azalea
2fdb6f15cb Merge branch 'master' into v1-dev 2024-02-26 17:06:35 -05:00
Azalea
043537a7b4 [F] Fix null 2024-02-26 16:38:54 -05:00
Azalea
e7643f3894 [F] Fix beans 2024-02-26 15:14:11 -05:00
Azalea
bb2c8ae8e5 [+] Ongeki adaptor 2024-02-26 15:08:45 -05:00
Azalea
1c8860c596 [F] fix shown ranks 2024-02-26 15:08:22 -05:00
Azalea
66e65fcd14 [+] chu3 rating composition 2024-02-26 12:09:17 -05:00
Azalea
7cef8f24db [-] Remove mysql 2024-02-26 11:59:53 -05:00
Azalea
a82f3a7b07 [U] Add maimai 140 fields 2024-02-26 11:59:30 -05:00
Azalea
4f41068c99 Merge branch 'master' into v1-dev 2024-02-26 11:58:49 -05:00
Azalea
50dfb95c48 [+] Chusan user summary 2024-02-26 11:55:43 -05:00
Azalea
0b29ac00a7 [+] Generalize game trend & summary apis 2024-02-26 11:54:31 -05:00
Azalea
fcbe52539a [F] Fix extra slash in url 2024-02-26 00:42:29 -05:00
Azalea
bcd64286cd [U] Update migration 2024-02-26 00:38:14 -05:00
Azalea
b89147120c [+] Find session when validating request 2024-02-26 00:32:38 -05:00
Azalea
c9ffd3cd11 [F] Fix overlapping filter 2024-02-26 00:29:15 -05:00
Azalea
cd62f31c17 [+] Add authentication token at poweron 2024-02-26 00:29:00 -05:00
Azalea
98d63b880b [U] Update default config 2024-02-26 00:28:30 -05:00
Azalea
04e11b0fea [+] Keychip session 2024-02-26 00:23:51 -05:00
Azalea
a873b28d9b [O] Reject unauthenticated requests 2024-02-25 23:21:34 -05:00
Azalea
b1b2ff6b8c [+] Securing allnet requests 2024-02-25 23:09:56 -05:00
Azalea
cb96b5fa8f [M] Move game URLs 2024-02-25 21:42:58 -05:00
Azalea
eb960209bf [+] Rewrite billing 2024-02-25 21:11:52 -05:00
Azalea
51a0e46f8c [+] Rewrite allnet 2024-02-25 20:23:06 -05:00
Azalea
1251205fdd [+] Map extensions 2024-02-25 20:22:09 -05:00
Azalea
9a05629144 [O] Reduce duplicate code 2024-02-25 17:58:04 -05:00
Azalea
e0c71006d5 [O] Remake maimai2 user summary api 2024-02-25 17:22:12 -05:00
Azalea
3d716a516a [M] Rename field 2024-02-25 17:21:50 -05:00
Azalea
096648b2d7 [O] Simplify class name for logging 2024-02-23 02:14:33 -05:00
Azalea
02e57707de Merge branch 'master' into v1-dev 2024-02-23 00:12:08 -05:00
Azalea
af3aa497d1 [O] Better understanding of the data type of different games 2024-02-22 23:51:22 -05:00
Azalea
bb53d1448b [O] Better logging 2024-02-22 23:50:58 -05:00
Azalea
eccdd73908 [+] Backend card linking 2024-02-22 22:45:24 -05:00
Azalea
1d4e1a8be2 [O] Better logging 2024-02-22 22:45:08 -05:00
Azalea
8dc0f299a9 [-] Remove global logger field 2024-02-22 22:38:21 -05:00
Azalea
db1ffd5091 [-] Remove unnecessary beans 2024-02-22 22:37:07 -05:00
Azalea
64a27e5708 [O] Speed up bootup by making email init async 2024-02-22 22:36:48 -05:00
Azalea
214a356135 [O] Better logging 2024-02-22 22:19:21 -05:00
Azalea
84f7953f21 [F] Fix encryption 2024-02-22 22:19:12 -05:00
Azalea
a9a947203d [O] Reduce code 2024-02-22 21:22:08 -05:00
Azalea
8f250e755e [O] Refactor AimeDB 2024-02-22 20:55:13 -05:00
Azalea
30a7fa7ead [O] Convert more stuff to kotlin 2024-02-22 19:47:35 -05:00
Azalea
4324d655d2 [M] Refactor relevant stuff to kotlin 2024-02-22 19:33:51 -05:00
Azalea
da1be9226a [S] Bottom padding 2024-02-22 19:11:25 -05:00
Azalea
5597bf5d1e [O] Show confirm dialog when unlinking a card 2024-02-22 19:06:18 -05:00
Azalea
50029fbb24 [F] Fix TS errors 2024-02-22 19:00:14 -05:00
Azalea
f5c2dc747d [+] Unlink frontend 2024-02-22 18:31:59 -05:00
Azalea
dd55e336e4 [+] API for unlinking 2024-02-22 18:20:54 -05:00
Azalea
a001a45cc4 [M] Rename: Bind -> Link 2024-02-22 18:19:55 -05:00
Azalea
49320ff623 [M] Move LinkCard component 2024-02-22 18:17:23 -05:00
Azalea
8e898c50b4 [O] Ghost cards should not be guessed 2024-02-22 18:14:58 -05:00
Azalea
5fa93e2a2a [F] Fix lateinit issue 2024-02-22 18:11:22 -05:00
Azalea
178cca1611 [O] Reduce bits for DIVA 2024-02-22 17:31:10 -05:00
Azalea
9e543e2c5a [+] Add isGhost to card 2024-02-22 17:29:28 -05:00
Azalea
6a16e5534d [+] Sanitize card id when creating card 2024-02-22 17:29:13 -05:00
Azalea
2d1cad870b [S] Always put ghost card at the top 2024-02-22 17:07:01 -05:00
Azalea
3fdf255ca5 [F] Use bigint 2024-02-22 17:06:50 -05:00
Azalea
ec9225dbf2 [+] Iconify tools 2024-02-22 17:06:40 -05:00
Azalea
b469fe92dd [+] Link card when not found 2024-02-22 16:39:00 -05:00
Azalea
dbc54b016c [O] Check if already linked 2024-02-22 12:30:28 -05:00
Azalea
2cbad36f80 [+] Check card is linked 2024-02-22 12:26:27 -05:00
Azalea
8b21f33eb6 [-] Remove sensitive info 2024-02-22 10:52:09 -05:00
Azalea
1e8ff7dbc0 [+] Create new card if not exists 2024-02-22 10:51:49 -05:00
Azalea
0937915839 [+] Link card limit on the backend 2024-02-22 10:51:22 -05:00
Azalea
a128546954 [+] Add field isBound to Card 2024-02-22 10:31:13 -05:00
Azalea
806953d107 [S] Unify transition style 2024-02-22 10:30:43 -05:00
Azalea
afa39b29ed [+] Implement link conflict resolution 2024-02-22 10:30:28 -05:00
Azalea
4c899555dd [S] Style overlay 2024-02-22 10:00:28 -05:00
Azalea
3e8395b0c6 [+] Show card conflicts 2024-02-22 10:00:18 -05:00
Azalea
a620f02d57 [-] Remove test dialect 2024-02-21 17:43:03 -05:00
Azalea
79a078fb70 [+] Automatically invalidate token on expiry 2024-02-21 17:42:42 -05:00
Azalea
cac2f49b06 [+] Card summary sdk 2024-02-21 17:42:15 -05:00
Azalea
06993b9d66 [S] Style existing cards, display card type 2024-02-21 15:03:49 -05:00
Azalea
fce5ca592a [S] Style existing cards 2024-02-21 14:26:12 -05:00
Azalea
e7058cf3c8 [+] Show existing cards 2024-02-21 14:14:38 -05:00
Azalea
a2eeac786e [+] /user/me sdk 2024-02-21 14:14:28 -05:00
Azalea
ff7873313b [+] Check while typing 2024-02-21 13:59:40 -05:00
Azalea
f3b06ac0a6 [+] Input validation for card SN 2024-02-21 13:47:58 -05:00
Azalea
00e57fc17d [+] Bind card element 2024-02-21 13:37:13 -05:00
Azalea
c8cc59aaca [S] Adjust input visibility 2024-02-21 13:37:01 -05:00
Azalea
54057922f6 [O] Better clazz 2024-02-21 13:36:21 -05:00
Azalea
823eea1f0a [F] Fix card detection 2024-02-21 04:28:38 -05:00
Azalea
ae03a700de [S} Fix mobile view 2024-02-21 01:23:44 -05:00
Azalea
a089eade6e [S] Make letter spacing reponsive to font size 2024-02-21 01:15:52 -05:00
Azalea
f8fb3d8a70 [+] Router logo 2024-02-21 01:15:30 -05:00
Azalea
00a75f154e [F] Fix asking for email confirmation when email feature is disabled 2024-02-21 01:14:58 -05:00
Azalea
8d2313d799 [-] Remove comments in build.gradle 2024-02-21 00:44:44 -05:00
Azalea
0b8384fc3b [+] Home page 2024-02-21 00:42:04 -05:00
Azalea
73ab9efdb4 [S] Globalize content css 2024-02-21 00:15:31 -05:00
Azalea
705f69510b [F] Fix import, clear query param 2024-02-21 00:15:17 -05:00
Azalea
f7e0a33935 [+] Verify email workflow 2024-02-21 00:11:45 -05:00
Azalea
729015d719 [+] SDK confirmEmail 2024-02-21 00:11:31 -05:00
Azalea
d83127a265 [+] Check email confirmation on login 2024-02-21 00:02:58 -05:00
Azalea
6e8f7ae698 [F] Fix one-to-one relationship in email confirmation 2024-02-21 00:02:04 -05:00
Azalea
17ee24286c [+] Add a verify email state, state switching animation 2024-02-20 23:19:17 -05:00
Azalea
133140bf71 [O] Rename home to welcome 2024-02-20 18:46:15 -05:00
Azalea
5fafbf9ee8 [+] Login form 2024-02-20 18:44:15 -05:00
Azalea
6085da15a4 [S] Style input 2024-02-20 18:43:56 -05:00
Azalea
b93caf1839 [U] yarn upgrade 2024-02-20 18:43:42 -05:00
Azalea
c9787a521b [+] SDK 2024-02-20 18:41:59 -05:00
Azalea
26cabef74c [F] Export turnstile site key 2024-02-20 18:32:42 -05:00
Azalea
8d2474768b [S] More color variables 2024-02-20 18:32:25 -05:00
Azalea
a87146a401 [+] Svelte turnstile 2024-02-20 18:29:59 -05:00
Azalea
bbf5ee5395 [+] Add mariadb migration 2024-02-20 17:43:35 -05:00
Azalea
103ae607be [F] Forgot to save ;-; 2024-02-20 16:26:10 -05:00
Azalea
6f63998000 [O] Ignore serial id 2024-02-20 16:19:06 -05:00
Azalea
a94952babc [O] Reduce code 2024-02-20 16:18:43 -05:00
Azalea
4b8385419e [O] Limit exposure of fields 2024-02-20 16:16:11 -05:00
Azalea
878a543818 [F] Fix infinite recursion on serializing user card 2024-02-20 16:12:08 -05:00
Azalea
e7337777cd [F] Set last login time in login 2024-02-20 16:11:50 -05:00
Azalea
fa1ed52c32 [+] Bind card 2024-02-20 16:06:46 -05:00
Azalea
eda3fccb51 [F] Fix RNG 2024-02-20 16:01:07 -05:00
Azalea
ec55fae1ec [+] Settings API 2024-02-20 15:47:25 -05:00
Azalea
c88a98e355 [+] Separate user validator 2024-02-20 15:47:17 -05:00
Azalea
0567e0f251 [+] Add @ API macro 2024-02-20 15:46:48 -05:00
Azalea
befa7d0e8e [+] Extend jwt auth block 2024-02-20 15:45:25 -05:00
Azalea
91913da205 [+] Card summary 2024-02-20 02:01:15 -05:00
Azalea
cd8677a26d [+] Try lookup id function 2024-02-19 21:49:55 -05:00
Azalea
adf091e300 [O] Use randExtId 2024-02-19 21:49:09 -05:00
Azalea
aa3b831a68 [O] Separate randExtId 2024-02-19 21:48:42 -05:00
Azalea
ab075c0554 [F] Fix ghost card column 2024-02-19 21:46:43 -05:00
Azalea
4202012bbd [+] Confirm email 2024-02-19 21:05:24 -05:00
Azalea
739854935d [+] Create ghost card on registration 2024-02-19 21:05:17 -05:00
Azalea
3d88e734df [+] Add ghostCard for user 2024-02-19 21:04:57 -05:00
Azalea
a716a69b8b [F] Fix tests 2024-02-19 21:03:38 -05:00
Azalea
7ac7aacb6c [O] Refactor code 2024-02-19 20:58:16 -05:00
Azalea
7368001e3b [+] Send confirmation email on register 2024-02-19 06:59:32 -05:00
Azalea
574e0b4074 [+] Generate email 2024-02-19 06:59:13 -05:00
Azalea
5d258eb8e1 [+] Computed name for user 2024-02-19 06:59:00 -05:00
Azalea
5715fa97f7 [+] Email confirmation table 2024-02-19 06:58:51 -05:00
Azalea
7fe869b98b [+] Email template 2024-02-19 06:58:40 -05:00
Azalea
37aaa30387 [+] Add email web host option 2024-02-19 06:58:26 -05:00
Azalea
c3b2d7653f [+] Add email border 2024-02-19 06:34:59 -05:00
Azalea
cb22161156 [+] Add email confirmation field 2024-02-19 05:09:36 -05:00
Azalea
6ad06c2d75 [F] Fix json parsing 2024-02-19 05:09:23 -05:00
Azalea
b291dd0ad7 [-] Remove dialect 2024-02-19 04:54:44 -05:00
Azalea
3039a32f29 [F] Fix CORS 2024-02-19 04:51:01 -05:00
Azalea
a8f5380070 [-] Remove unnecessary mariadb dialect 2024-02-19 04:48:14 -05:00
Azalea
e37867b9db [O] Disable whitelabel error page 2024-02-19 04:43:09 -05:00
Azalea
110d6c81ee [+] Add /me endpoint 2024-02-19 04:42:50 -05:00
Azalea
3da44ce604 [-] Remove duplicate email self test 2024-02-19 04:42:11 -05:00
Azalea
9770c15188 [+] JWT.auth 2024-02-19 04:39:40 -05:00
Azalea
cc568d9569 [F] Fix token too short 2024-02-19 04:39:28 -05:00
Azalea
500a4b0b7e [M] Move security config 2024-02-19 04:35:08 -05:00
Azalea
55cfb7b358 [+] Login 2024-02-19 03:21:49 -05:00
Azalea
77b2f90259 [F] Fix response syntax limitation 2024-02-19 03:16:35 -05:00
Azalea
e962baaf48 [M] Move services to components package 2024-02-19 03:07:32 -05:00
Azalea
2cb5b18975 [+] JWT class 2024-02-19 03:06:42 -05:00
Azalea
32084eb1e7 [F] Fix: Email and username should ignore case 2024-02-19 03:06:30 -05:00
Azalea
2815d76b1d [+] JWT authentication settings 2024-02-19 02:40:00 -05:00
Azalea
9c4f146778 [O] More checks 2024-02-19 02:29:45 -05:00
Azalea
7b89016359 [F] Fix runtime dependency issue 2024-02-19 02:26:20 -05:00
Azalea
c7a4902af0 [F] Ignore geoip not found error 2024-02-19 02:26:01 -05:00
Azalea
6f9b686317 [F] Fix error reporting 2024-02-19 02:25:50 -05:00
Azalea
16550e7a83 [O] Better error messages 2024-02-19 02:25:41 -05:00
Azalea
a9aa47e390 [U] Update config 2024-02-19 01:57:56 -05:00
Azalea
0846fb94db [F] Fix null case 2024-02-19 01:57:42 -05:00
Azalea
4c3aafd266 [O] Unwrap spaghetti code 2024-02-19 01:49:29 -05:00
Azalea
1e606f8b85 [+] Add username check 2024-02-19 01:38:42 -05:00
Azalea
58596377b1 [+] Add username field 2024-02-19 01:38:30 -05:00
Azalea
94a3234874 [O] CORS allow all 2024-02-19 01:37:50 -05:00
Azalea
7e9db5b52d [+] GeoIP service 2024-02-19 01:36:53 -05:00
Azalea
dc098d1ec7 [O] Make data dir if not exist 2024-02-19 01:36:39 -05:00
Azalea
195a8b4315 [+] GeoLite settings 2024-02-19 01:36:18 -05:00
Azalea
7df80bc56a [+] GeoIP library 2024-02-19 01:35:34 -05:00
Azalea
1c541a4adf [F] Fix rating calculation 2024-02-19 00:39:43 -05:00
Azalea
f29f563e50 [F] Fix null pointer 2024-02-19 00:39:27 -05:00
Azalea
3a94ef57e3 [+] Email service 2024-02-17 04:16:16 -05:00
Azalea
db8d8db280 [+] Email settings 2024-02-17 04:04:39 -05:00
Azalea
fdcef95d07 [+] Simple java mail 2024-02-17 04:04:14 -05:00
Azalea
7b1d9a777d [+] Email settings 2024-02-17 03:45:49 -05:00
Azalea
3cd8764dbf [+] Turnstile site key 2024-02-17 03:30:05 -05:00
Azalea
32826440cb [F] Fix spring autowire for AquaNetUser 2024-02-17 01:43:52 -05:00
Azalea
a65fa8cf10 [U] Finalize mysql migration guide 2024-02-17 01:33:34 -05:00
Azalea
0ae29b1920 [+] Write mysql migration guide 2024-02-17 01:07:23 -05:00
Azalea
5748a11788 [-] Completely drop mysql support 2024-02-17 01:07:09 -05:00
Azalea
7d3579af4f [-] Drop mysql support 2024-02-17 00:37:06 -05:00
Azalea
e0dc3bd1f4 [+] Validate captcha 2024-02-17 00:31:40 -05:00
Azalea
6200c56144 [+] User registration endpoint 2024-02-17 00:25:40 -05:00
Azalea
0b4a0eeb55 [+] AquaNetUser JPA entity 2024-02-17 00:25:19 -05:00
Azalea
467f5bd2eb [U] Update config 2024-02-17 00:25:04 -05:00
Azalea
322d90adfa [+] Automatic obtain request ip 2024-02-17 00:24:35 -05:00
Azalea
11eb7c058f [U] Update gitignore 2024-02-17 00:22:48 -05:00
Azalea
361b251952 [+] More extensions 2024-02-16 23:57:31 -05:00
Azalea
3d503971ae [+] Turnstile utility class 2024-02-16 23:56:12 -05:00
Azalea
9faabba361 [+] Turnstile settings 2024-02-16 23:55:44 -05:00
Azalea
f33629aba1 [+] Ktor dependency 2024-02-16 23:55:30 -05:00
Azalea
4a9b9d57e4 [O] Optimize imports 2024-02-16 17:51:36 -05:00
Azalea
98c3f0ce5b [F] Fix SNI 2024-02-16 17:45:41 -05:00
Azalea
78a3082bcb [U] Use kotlin entrypoint 2024-02-16 17:07:58 -05:00
Azalea
b64af43a7e [F] Fix security config 2024-02-16 17:07:45 -05:00
Azalea
1bcacbfebe [U] Update default config 2024-02-16 17:06:53 -05:00
Azalea
f32db6c83b [U] Update config 2024-02-16 17:06:39 -05:00
Azalea
437b638973 [M] javax -> jakarta 2024-02-16 17:05:43 -05:00
Azalea
670918efd3 [+] Migrate dependencies to Spring 3 2024-02-16 17:03:52 -05:00
950 changed files with 16277 additions and 68954 deletions

BIN
.github/workflows/DATA vendored

Binary file not shown.

View File

@@ -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
View File

@@ -75,4 +75,8 @@ gradle-app.setting
### Gradle Patch ###
# Java heap dump
*.hprof
*.hprof
.jpb
src/main/resources/meta/*/*.json
*.log.*.gz
*.salive

View File

@@ -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" />

View File

@@ -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

View File

@@ -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; }
}
}
}

View File

@@ -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!");
}

View 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;
}
}
}

View 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
View 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

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View 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;
}

Binary file not shown.

View 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;
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 894 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 KiB

Binary file not shown.

View File

@@ -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>

View File

@@ -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)

View 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>

View 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

View 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>

View 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>

View File

@@ -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"

View File

@@ -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
View 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")}

View 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
View 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 }

View File

@@ -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 })
}

View File

@@ -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

View 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
View 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}` }),
}

View File

@@ -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)
}

View File

@@ -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">&nbsp;</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>

View 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>

View 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>

View File

@@ -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>

View 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>

View 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>

View File

@@ -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>

View 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>

View File

@@ -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

View File

@@ -1,2 +1,3 @@
/// <reference types="svelte" />
/// <reference types="vite/client" />
declare const APP_VERSION: string;

View File

@@ -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

View File

@@ -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
View 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.

View File

@@ -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.

View File

@@ -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
}

View File

@@ -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

View File

@@ -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
View 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

View File

@@ -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
View 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.

View File

@@ -2,4 +2,4 @@
* This file was generated by the Gradle 'init' task.
*/
rootProject.name = "aqua"
rootProject.name = "AquaDX"

View File

@@ -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
View 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
// }
//}

View File

@@ -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();
}
}

View 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()
}

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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(" ", ""));

View File

@@ -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);
// }
}

View File

@@ -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() {

View File

@@ -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() {

View File

@@ -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);

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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)
)
}
}

View File

@@ -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();

View File

@@ -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."));

View File

@@ -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

View File

@@ -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) {

View File

@@ -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;

View 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())
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -7,7 +7,7 @@ import lombok.Data;
*/
@Data
public class PlayerInfo {
private int pdId;
private long pdId;
private String playerName;
private int vocaloidPoints;
}

View File

@@ -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)
*/

View File

@@ -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;
}

View 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())
}

View File

@@ -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;
}

View File

@@ -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;
/**

View File

@@ -6,7 +6,6 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
/**
* @author samnyan (privateamusement@protonmail.com)

View File

@@ -6,7 +6,6 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
/**
* @author samnyan (privateamusement@protonmail.com)

View 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!!) }
}
}
}
}
}

View 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
)
}
}

View 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
}
}
}

View 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