Compare commits
456 Commits
frontend_u
...
idac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d768a1d062 | ||
|
|
8748b3d949 | ||
|
|
e3f412c40c | ||
|
|
c5bced902d | ||
|
|
b8bf9ca506 | ||
|
|
d1b377eef3 | ||
|
|
8b2c73b8f6 | ||
|
|
c1013b38bc | ||
|
|
9379172791 | ||
|
|
c741c052e9 | ||
|
|
7e709db0cc | ||
|
|
10582cc1fc | ||
|
|
9692aea87b | ||
|
|
bfebe69a74 | ||
|
|
87c7c91e3a | ||
|
|
de2e2349e6 | ||
|
|
3613f4dbd2 | ||
|
|
3cd3910b0d | ||
|
|
942b636b3e | ||
|
|
75bf8f4cb7 | ||
|
|
40a0817009 | ||
|
|
346f82a32a | ||
|
|
f71591e622 | ||
|
|
7066651549 | ||
|
|
a1a43130bf | ||
|
|
88b3cfc750 | ||
|
|
a0fba8c3a4 | ||
|
|
84fa139357 | ||
|
|
752bb3f244 | ||
|
|
b63c2c2d12 | ||
|
|
6557cac55b | ||
|
|
c4deff9d1c | ||
|
|
bc2b883fa9 | ||
|
|
86f419278d | ||
|
|
3fe8bc8c62 | ||
|
|
3557578bcd | ||
|
|
6f654f8ba9 | ||
|
|
3559d56ccf | ||
|
|
e69149632f | ||
|
|
bd97428166 | ||
|
|
84c059ed75 | ||
|
|
91f49f52cc | ||
|
|
f708b8ea4f | ||
|
|
b83399cef4 | ||
|
|
43d0edf036 | ||
|
|
4363b8321b | ||
|
|
b7f56c20a7 | ||
|
|
b462a2720a | ||
|
|
89c2b324dc | ||
|
|
4149944d56 | ||
|
|
d4e00781c8 | ||
|
|
50f42f850a | ||
|
|
d38207cdc3 | ||
|
|
b0f603cbd8 | ||
|
|
864b749f20 | ||
|
|
9844b7ebdb | ||
|
|
77dfec0483 | ||
|
|
c7aa3f2479 | ||
|
|
9f67079614 | ||
|
|
d2177bdbed | ||
|
|
ce5888227e | ||
|
|
83b981917e | ||
|
|
3a710c671e | ||
|
|
348edef0f5 | ||
|
|
60e9cd2edd | ||
|
|
404496191d | ||
|
|
8864b2625d | ||
|
|
200bfe7d84 | ||
|
|
6ed80ecbab | ||
|
|
9a021d4005 | ||
|
|
66c9548dd2 | ||
|
|
029be55cc0 | ||
|
|
5c9570d421 | ||
|
|
8d416d7633 | ||
|
|
a15ad77f20 | ||
|
|
384924335c | ||
|
|
58c7d3f22d | ||
|
|
1cced47c1d | ||
|
|
4b2bce9d12 | ||
|
|
220ba8ba54 | ||
|
|
1c3f52974d | ||
|
|
de013766f6 | ||
|
|
a4262b9285 | ||
|
|
f41eb907ef | ||
|
|
a3b1d07c73 | ||
|
|
7cf8b4fd5f | ||
|
|
e36989e7cc | ||
|
|
1003651fb4 | ||
|
|
97e5c29cf5 | ||
|
|
cdc1b302f2 | ||
|
|
ff26485c93 | ||
|
|
e80b0671c2 | ||
|
|
ae09c2ad48 | ||
|
|
74a671aff5 | ||
|
|
9a7d5bc689 | ||
|
|
fc3f0900b3 | ||
|
|
19fd008598 | ||
|
|
ba7cb07208 | ||
|
|
31969221e2 | ||
|
|
df4c667adf | ||
|
|
72bac55353 | ||
|
|
bb752563cc | ||
|
|
a676e42a59 | ||
|
|
6dd46a5aa7 | ||
|
|
fb36cc9b07 | ||
|
|
3bfc9cc845 | ||
|
|
06e7288cad | ||
|
|
d01ceab92f | ||
|
|
0a56207e90 | ||
|
|
d672edb266 | ||
|
|
805b8f5b3e | ||
|
|
8a99f94c93 | ||
|
|
5e6efbd092 | ||
|
|
be0e407ebe | ||
|
|
c680c2d4e9 | ||
|
|
261d09aaef | ||
|
|
2c1958eb04 | ||
|
|
37304500a5 | ||
|
|
e6dad1cb34 | ||
|
|
2d95e29f3c | ||
|
|
f8c77e69ed | ||
|
|
9dab26b122 | ||
|
|
e27ac4b81f | ||
|
|
05586df08a | ||
|
|
edd3ce8ead | ||
|
|
07cbbcc377 | ||
|
|
4bc77a8ef6 | ||
|
|
1c22c6bec0 | ||
|
|
7b49a8ab58 | ||
|
|
f65aa4d60a | ||
|
|
4b9db8be3b | ||
|
|
5c124a7d61 | ||
|
|
b0536e45ed | ||
|
|
08c831ff8c | ||
|
|
0c3924a8f0 | ||
|
|
4550cb2af5 | ||
|
|
4933ad72f2 | ||
|
|
cc68b7f6c6 | ||
|
|
14fa0f5e8e | ||
|
|
b056ff218d | ||
|
|
01591a0456 | ||
|
|
b4b40869c1 | ||
|
|
c9dfbc77c4 | ||
|
|
1b3e43b918 | ||
|
|
f5c77f04fa | ||
|
|
27bf51f9f8 | ||
|
|
343424e26a | ||
|
|
7639c2ef74 | ||
|
|
9ee58cd444 | ||
|
|
e08acf8ee4 | ||
|
|
62937584bc | ||
|
|
c0078b252f | ||
|
|
4c1603db09 | ||
|
|
3dd23d4b53 | ||
|
|
a883450e5c | ||
|
|
acc3a63030 | ||
|
|
3426a37d2d | ||
|
|
f225b732f2 | ||
|
|
a42993aca7 | ||
|
|
39031757f4 | ||
|
|
a2975fc979 | ||
|
|
b1026fdec0 | ||
|
|
5f33b2d3e4 | ||
|
|
0b38778c19 | ||
|
|
19baf05d7b | ||
|
|
32419faf01 | ||
|
|
bf7d709b49 | ||
|
|
37b1f41b44 | ||
|
|
7b8611cee3 | ||
|
|
d0f8568e17 | ||
|
|
fd0fefa48b | ||
|
|
b5462276f5 | ||
|
|
626ea58f75 | ||
|
|
40e6e42c87 | ||
|
|
a2d238857c | ||
|
|
a6b315185d | ||
|
|
9b9d1f664d | ||
|
|
b6d1f2cd3a | ||
|
|
0060122613 | ||
|
|
97963adf38 | ||
|
|
1bfe3584b1 | ||
|
|
ec4ae98beb | ||
|
|
a0dc8421eb | ||
|
|
ed731e7407 | ||
|
|
15204f8d8a | ||
|
|
32362dbe1e | ||
|
|
59db7ad44a | ||
|
|
5491266a85 | ||
|
|
283cf41bce | ||
|
|
26cdc6c10f | ||
|
|
c1412ca9a8 | ||
|
|
21492fbfb3 | ||
|
|
82c5ae3ad7 | ||
|
|
7d0cf6a0c4 | ||
|
|
95dacccc0e | ||
|
|
dace9ae980 | ||
|
|
6ef8fc4d57 | ||
|
|
262155f83f | ||
|
|
936316f129 | ||
|
|
104a483f4c | ||
|
|
29a1dac983 | ||
|
|
8c0ebbd21b | ||
|
|
d024b2eeb8 | ||
|
|
ed2d306dee | ||
|
|
ffc0f2fa5e | ||
|
|
a1569bca9d | ||
|
|
cc86d4509e | ||
|
|
216fa49f8b | ||
|
|
662fd05d24 | ||
|
|
46f61325cb | ||
|
|
e7fb9ce07d | ||
|
|
fbc0e489bc | ||
|
|
6774716e06 | ||
|
|
2efdf79b87 | ||
|
|
cf0c34cafb | ||
|
|
5fd966eaad | ||
|
|
cdd67ffbc3 | ||
|
|
7fb98118b2 | ||
|
|
fe25359e8e | ||
|
|
468c99c3c2 | ||
|
|
6504f120ad | ||
|
|
d769285d89 | ||
|
|
ad61bb3d9b | ||
|
|
e124d5e52e | ||
|
|
989cbdf748 | ||
|
|
6ea8cca1a2 | ||
|
|
e941c6a606 | ||
|
|
ecd63c02e3 | ||
|
|
581c1506a1 | ||
|
|
4d5823a4f7 | ||
|
|
df9ba9beda | ||
|
|
8bd361d3e3 | ||
|
|
0eee2e92a8 | ||
|
|
b4a0d331d4 | ||
|
|
77b22cd77d | ||
|
|
25ab37561d | ||
|
|
d467070ba7 | ||
|
|
e7f35db3a4 | ||
|
|
0422a2bfd4 | ||
|
|
05be10dabc | ||
|
|
9a017f75e9 | ||
|
|
69cd79003c | ||
|
|
9605c13e8e | ||
|
|
9c40bad89c | ||
|
|
d34bc424c2 | ||
|
|
913987da1d | ||
|
|
7b58cae214 | ||
|
|
cdb040a65e | ||
|
|
d3b01e03c5 | ||
|
|
a7e0099294 | ||
|
|
dcebc5c21a | ||
|
|
04c708be8c | ||
|
|
67c9ac5ef9 | ||
|
|
328f64ad8a | ||
|
|
e0aeb2ac21 | ||
|
|
3d8ffaa098 | ||
|
|
7eb095cdb4 | ||
|
|
9c49a4f54a | ||
|
|
dffa11f420 | ||
|
|
1ba14da90c | ||
|
|
0003ca4412 | ||
|
|
0ca041c042 | ||
|
|
25528203fe | ||
|
|
a06d1aaf8c | ||
|
|
e035806b41 | ||
|
|
a9f72cec69 | ||
|
|
22cf3f83a6 | ||
|
|
ee45c55f66 | ||
|
|
2c275d1130 | ||
|
|
b19288fb7e | ||
|
|
a83edee657 | ||
|
|
d1a7b898a7 | ||
|
|
e561f8f15d | ||
|
|
c9a20b2433 | ||
|
|
1034a505f0 | ||
|
|
565dc38e9a | ||
|
|
1a4dc88c8a | ||
|
|
005adfae38 | ||
|
|
80ae372ccf | ||
|
|
e461ffe587 | ||
|
|
b56a5f020b | ||
|
|
d663b1ef7c | ||
|
|
31d39ffb37 | ||
|
|
530016ef84 | ||
|
|
f81c53558e | ||
|
|
d2e2c14074 | ||
|
|
1897e8002d | ||
|
|
4da886a083 | ||
|
|
4bedf71d3d | ||
|
|
84b44d2a02 | ||
|
|
b516da2e6e | ||
|
|
a767646361 | ||
|
|
3596f0f34c | ||
|
|
ab3b6a9814 | ||
|
|
c384a8d1f5 | ||
|
|
9bc76279f1 | ||
|
|
9026c25fcc | ||
|
|
ff48438b85 | ||
|
|
e1a70d1a06 | ||
|
|
1ff0e4e2c6 | ||
|
|
c23ab1438e | ||
|
|
773729dc0c | ||
|
|
0e739b2077 | ||
|
|
c4f2232457 | ||
|
|
9fdd96b717 | ||
|
|
c370542761 | ||
|
|
a299ba98ac | ||
|
|
44755d4d0f | ||
|
|
3d62b9d372 | ||
|
|
18bf250fd7 | ||
|
|
0fa8fe06f0 | ||
|
|
4cd1181ef7 | ||
|
|
8d190ce7f0 | ||
|
|
81d588cbc7 | ||
|
|
e6801c1c46 | ||
|
|
840db275fa | ||
|
|
8769c99f4a | ||
|
|
3c06f46644 | ||
|
|
95234a421c | ||
|
|
c425ca1ea8 | ||
|
|
b0ebcaf0cf | ||
|
|
8560c05928 | ||
|
|
580a9faae8 | ||
|
|
9537331d1c | ||
|
|
4047a38370 | ||
|
|
eaf64dbd50 | ||
|
|
e769404e16 | ||
|
|
eb10bc2560 | ||
|
|
4b22bd734e | ||
|
|
94c326a27d | ||
|
|
cb8eaae2c0 | ||
|
|
c2a330f42c | ||
|
|
fbd1d7cb1c | ||
|
|
fbd12fd6e2 | ||
|
|
40e6c018e9 | ||
|
|
4c3852d6d4 | ||
|
|
acac2c4670 | ||
|
|
1224baeade | ||
|
|
040742a9a4 | ||
|
|
b4a0736f7b | ||
|
|
a7b0b1be49 | ||
|
|
dde397a96c | ||
|
|
f99bf7d1ed | ||
|
|
bd3e1918aa | ||
|
|
ffc0a07f6d | ||
|
|
ab640760a9 | ||
|
|
a8f06ee266 | ||
|
|
3f192bd84a | ||
|
|
4b83d3f316 | ||
|
|
1b5c335f4e | ||
|
|
680223dba2 | ||
|
|
4fc4d98a74 | ||
|
|
3259552c29 | ||
|
|
b5ce13d1f1 | ||
|
|
be86448b23 | ||
|
|
01182087e0 | ||
|
|
e9637c94de | ||
|
|
37a6502dc9 | ||
|
|
d1259509ad | ||
|
|
7f1ff60b9d | ||
|
|
12d0a3f927 | ||
|
|
5420076c8e | ||
|
|
a3d2955fce | ||
|
|
990d60cf27 | ||
|
|
21cb37001b | ||
|
|
32903d979e | ||
|
|
3f8c62044c | ||
|
|
719ae9cfb1 | ||
|
|
8fcd227b33 | ||
|
|
b5e3bf6f80 | ||
|
|
81f7106264 | ||
|
|
54c77c047e | ||
|
|
93b8b86b55 | ||
|
|
eaa2652647 | ||
|
|
46f83b9925 | ||
|
|
862907b82a | ||
|
|
8c114532e8 | ||
|
|
300cd10abf | ||
|
|
4f0a5f60ab | ||
|
|
9cff321857 | ||
|
|
8d289ca066 | ||
|
|
e18b87ee5c | ||
|
|
540d7fc2c2 | ||
|
|
3ef40fe85e | ||
|
|
35a7525f74 | ||
|
|
d49997f832 | ||
|
|
c78a62de14 | ||
|
|
480551f942 | ||
|
|
d6e4db48f4 | ||
|
|
346b74e51b | ||
|
|
aa9d48ccc9 | ||
|
|
dc02d60690 | ||
|
|
6d592dcbc7 | ||
|
|
3ee1801ee6 | ||
|
|
31188ccce6 | ||
|
|
ab10f27cb4 | ||
|
|
0a12e93593 | ||
|
|
bad106ceba | ||
|
|
7fc5544c15 | ||
|
|
d55ada2538 | ||
|
|
bfaadff9f6 | ||
|
|
1996f3f356 | ||
|
|
56ddd3b1cc | ||
|
|
41fcadfd55 | ||
|
|
06d95c8c5f | ||
|
|
b5ccd67940 | ||
|
|
1f65cfd2eb | ||
|
|
9681f86e33 | ||
|
|
8ccb7f08f2 | ||
|
|
0e664e03f9 | ||
|
|
d641705273 | ||
|
|
3d13eb1698 | ||
|
|
33e0288e5e | ||
|
|
ee5f13a3bb | ||
|
|
4ba01cdf20 | ||
|
|
7033234b23 | ||
|
|
e0265485ff | ||
|
|
38c1c31cf5 | ||
|
|
91791813dc | ||
|
|
1497bf4a95 | ||
|
|
aa17f99252 | ||
|
|
50de0916d4 | ||
|
|
2affd0aae9 | ||
|
|
4a916cb4d1 | ||
|
|
6c98f5f0a7 | ||
|
|
090b3148d8 | ||
|
|
ed46ea33e3 | ||
|
|
d584b93ca5 | ||
|
|
d5d2803cc0 | ||
|
|
16d801aff5 | ||
|
|
147d7adaaf | ||
|
|
1f545aed41 | ||
|
|
eaf9e0cf09 | ||
|
|
0d7409906a | ||
|
|
5cccc9224a | ||
|
|
2a52e391d6 | ||
|
|
fc947d36a5 | ||
|
|
5f586379ca | ||
|
|
3c7ceabf4e | ||
|
|
1bc8648e35 | ||
|
|
eecd3a829d | ||
|
|
b42e8ab76c | ||
|
|
043ff17008 | ||
|
|
c01d3f49f5 | ||
|
|
75842b5a88 | ||
|
|
fe4dfe369b | ||
|
|
02040300b8 | ||
|
|
780a96ec15 | ||
|
|
b8b93a8d51 | ||
|
|
a0e24c6742 | ||
|
|
90024ddbd9 | ||
|
|
be00ad8e96 | ||
|
|
e2129b45b7 | ||
|
|
670747cf48 | ||
|
|
83f09e180e | ||
|
|
a83b717821 |
3
.gitattributes
vendored
Normal file
3
.gitattributes
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
*.csv binary
|
||||||
|
*.txt binary
|
||||||
|
*.json binary
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
FROM python:3.9.15-slim-bullseye
|
FROM python:3.9.15-slim-bullseye
|
||||||
|
|
||||||
RUN apt update && apt install default-libmysqlclient-dev build-essential libtk nodejs npm -y
|
RUN apt update && apt install default-libmysqlclient-dev build-essential libtk nodejs npm pkg-config -y
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY requirements.txt requirements.txt
|
COPY requirements.txt requirements.txt
|
||||||
@@ -12,10 +12,10 @@ RUN chmod +x entrypoint.sh
|
|||||||
|
|
||||||
COPY index.py index.py
|
COPY index.py index.py
|
||||||
COPY dbutils.py dbutils.py
|
COPY dbutils.py dbutils.py
|
||||||
|
COPY read.py read.py
|
||||||
ADD core core
|
ADD core core
|
||||||
ADD titles titles
|
ADD titles titles
|
||||||
ADD config config
|
ADD logs logs
|
||||||
ADD log log
|
|
||||||
ADD cert cert
|
ADD cert cert
|
||||||
|
|
||||||
ENTRYPOINT [ "/app/entrypoint.sh" ]
|
ENTRYPOINT [ "/app/entrypoint.sh" ]
|
||||||
13
LICENSE.txt
Normal file
13
LICENSE.txt
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||||
|
Version 2, December 2004
|
||||||
|
|
||||||
|
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
|
||||||
|
|
||||||
|
Everyone is permitted to copy and distribute verbatim or modified
|
||||||
|
copies of this license document, and changing it is allowed as long
|
||||||
|
as the name is changed.
|
||||||
|
|
||||||
|
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. You just DO WHAT THE FUCK YOU WANT TO.
|
||||||
118
changelog.md
118
changelog.md
@@ -1,6 +1,124 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
Documenting updates to ARTEMiS, to be updated every time the master branch is pushed to.
|
Documenting updates to ARTEMiS, to be updated every time the master branch is pushed to.
|
||||||
|
|
||||||
|
## 20240318
|
||||||
|
### CXB
|
||||||
|
+ Fixing handle_data_shop_list_detail_request for Sunrise S1
|
||||||
|
|
||||||
|
## 20240302
|
||||||
|
### SAO
|
||||||
|
+ Fixing new profile creation with right heroes and start VP
|
||||||
|
+ Fix to the Unanalyzed Log responses returning the wrong rewards
|
||||||
|
+ Documentation revised
|
||||||
|
|
||||||
|
## 20240226
|
||||||
|
### CXB
|
||||||
|
+ Fixing paths for rev.py
|
||||||
|
+ Changed encoding for handle_data_item_list_icon_request
|
||||||
|
|
||||||
|
## 20240202
|
||||||
|
### SAO
|
||||||
|
+ Added reader assets and edited the game specific documentation
|
||||||
|
|
||||||
|
## 20240118
|
||||||
|
### System
|
||||||
|
+ Added game version names to the readme
|
||||||
|
|
||||||
|
## 20240109
|
||||||
|
### System
|
||||||
|
+ Removed `ADD config config` from dockerfile [#83](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/83) (Thanks zaphkito!)
|
||||||
|
|
||||||
|
### Aimedb
|
||||||
|
+ Fixed an error that resulted from trying to scan a banned or locked card
|
||||||
|
|
||||||
|
## 20240108
|
||||||
|
### System
|
||||||
|
+ Change how the underlying system handles URLs
|
||||||
|
+ This can now allow for things like version-specific, or even keychip-specific URLs
|
||||||
|
+ Specific changes to games are noted below
|
||||||
|
+ Fix docker files [#60](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/60) (Thanks Rylie!)
|
||||||
|
+ Fix support for python 3.8 - 3.10
|
||||||
|
|
||||||
|
### Aimedb
|
||||||
|
+ Add support for SegaAuth key in games that support it (for now only Chunithm)
|
||||||
|
+ This is a JWT that is sent to games, by Aimedb, that the games send to their game server, to verify that the access code the game is sending to the server was obtained via aimedb.
|
||||||
|
+ Requires a base64-encoded secret to be set in the `core.yaml`
|
||||||
|
|
||||||
|
### Chunithm
|
||||||
|
+ Fix Air support
|
||||||
|
+ Add saving for userRecentPlayerList
|
||||||
|
+ Add support for SegaAuthKey
|
||||||
|
+ Fix a bug arising if a user set their name to be 'true' or 'false'
|
||||||
|
+ Add support for Sun+ [#78](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/78) (Thanks EmmyHeart!)
|
||||||
|
+ Add `matching` section to `chuni.yaml`
|
||||||
|
+ ~~Change `udpHolePunchUri` and `reflectorUri` to be STUN and TURN servers~~ Reverted
|
||||||
|
+ Imrpove `GetGameSetting` request handling for different versions
|
||||||
|
+ Fix issue where songs would not always return all scores [#92](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/92) (Thanks Kumubou!)
|
||||||
|
|
||||||
|
### maimai DX
|
||||||
|
+ Fix user charges failing to save
|
||||||
|
|
||||||
|
### maimai
|
||||||
|
+ Made it functional
|
||||||
|
|
||||||
|
### CXB
|
||||||
|
+ Improvements to request dispatching
|
||||||
|
+ Add support for non-omnimix music lists
|
||||||
|
|
||||||
|
|
||||||
|
### IDZ
|
||||||
|
+ Fix news urls in accordance with the system change to URLs
|
||||||
|
|
||||||
|
### Initial D THE ARCADE
|
||||||
|
+ Added support for Initial D THE ARCADE S2
|
||||||
|
+ Story mode progress added
|
||||||
|
+ Bunta Challenge/Touhou Project modes added
|
||||||
|
+ Time Trials added
|
||||||
|
+ Leaderboards added, but doesn't refresh sometimes
|
||||||
|
+ Theory of Street mode added (with CPUs)
|
||||||
|
+ Play Stamp/Timetrial events added
|
||||||
|
+ Frontend to download profile added
|
||||||
|
+ Importer to import profiles added
|
||||||
|
|
||||||
|
### ONGEKI
|
||||||
|
+ Now supports HTTPS on a per-version basis
|
||||||
|
+ Merg PR [#61](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/61) (Thanks phantomlan!)
|
||||||
|
+ Add Ranking Event Support
|
||||||
|
+ Add reward list support
|
||||||
|
+ Add version segregation to Event Ranking, Tech Challenge, and Music Ranking
|
||||||
|
+ Now stores ClientTestmode and ClientSetting data
|
||||||
|
+ Fix mission points not adding correctly [#68](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/68) (Thanks phantomlan!)
|
||||||
|
+ Fix tech challenge [#70](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/70) (Thanks phantomlan!)
|
||||||
|
|
||||||
|
### SAO
|
||||||
|
+ Change endpoint in accordance with the system change to URLs
|
||||||
|
+ Update request header class to be more accurate
|
||||||
|
+ Encrypted requests are now supported
|
||||||
|
+ Change to using handler classes instead of raw structs for simplicity
|
||||||
|
|
||||||
|
### Wacca
|
||||||
|
+ Fix a server error causing a seperate error that casued issues
|
||||||
|
+ Add better error printing
|
||||||
|
+ Add better request validation
|
||||||
|
+ Fix HousingStartV2
|
||||||
|
+ Fix Lily's housing/get handler
|
||||||
|
|
||||||
|
## 20231107
|
||||||
|
### CXB
|
||||||
|
+ Hotfix `render_POST` sometimes failing to read the request body on large requests
|
||||||
|
|
||||||
|
## 20231106
|
||||||
|
### CXB
|
||||||
|
+ Hotfix `render_POST` function signature signature
|
||||||
|
+ Hotfix `handle_action_addenergy_request` hard failing if `get_energy` returns None
|
||||||
|
|
||||||
|
## 20231015
|
||||||
|
### maimai DX
|
||||||
|
+ Added support for FESTiVAL PLUS
|
||||||
|
|
||||||
|
### Card Maker
|
||||||
|
+ Added support for maimai DX FESTiVAL PLUS
|
||||||
|
|
||||||
## 20230716
|
## 20230716
|
||||||
### General
|
### General
|
||||||
+ Docker files added (#19)
|
+ Docker files added (#19)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from core.config import CoreConfig
|
from core.config import CoreConfig
|
||||||
from core.allnet import AllnetServlet
|
from core.allnet import AllnetServlet, BillingServlet
|
||||||
from core.aimedb import AimedbFactory
|
from core.aimedb import AimedbServlette
|
||||||
from core.title import TitleServlet
|
from core.title import TitleServlet
|
||||||
from core.utils import Utils
|
from core.utils import Utils
|
||||||
from core.mucha import MuchaServlet
|
from core.mucha import MuchaServlet
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ class ADBHeader:
|
|||||||
magic, protocol_ver, cmd, length, status, game_id, store_id, keychip_id = struct.unpack_from("<5H6sI12s", data)
|
magic, protocol_ver, cmd, length, status, game_id, store_id, keychip_id = struct.unpack_from("<5H6sI12s", data)
|
||||||
head = cls(magic, protocol_ver, cmd, length, status, game_id, store_id, keychip_id)
|
head = cls(magic, protocol_ver, cmd, length, status, game_id, store_id, keychip_id)
|
||||||
|
|
||||||
if head.length != len(data):
|
if head.length > len(data):
|
||||||
raise ADBHeaderException(f"Length is incorrect! Expect {head.length}, got {len(data)}")
|
raise ADBHeaderException(f"Length is incorrect! Expect {head.length}, got {len(data)}")
|
||||||
|
|
||||||
return head
|
return head
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ class ADBFelicaLookup2Response(ADBBaseResponse):
|
|||||||
self.access_code = access_code if access_code is not None else "00000000000000000000"
|
self.access_code = access_code if access_code is not None else "00000000000000000000"
|
||||||
self.company = CompanyCodes.SEGA
|
self.company = CompanyCodes.SEGA
|
||||||
self.portal_status = PortalRegStatus.NO_REG
|
self.portal_status = PortalRegStatus.NO_REG
|
||||||
|
self.auth_key = [0] * 256
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_req(cls, req: ADBHeader, user_id: Union[int, None] = None, access_code: Union[str, None] = None) -> "ADBFelicaLookup2Response":
|
def from_req(cls, req: ADBHeader, user_id: Union[int, None] = None, access_code: Union[str, None] = None) -> "ADBFelicaLookup2Response":
|
||||||
@@ -76,7 +77,7 @@ class ADBFelicaLookup2Response(ADBBaseResponse):
|
|||||||
access_code = bytes.fromhex(self.access_code),
|
access_code = bytes.fromhex(self.access_code),
|
||||||
portal_status = self.portal_status.value,
|
portal_status = self.portal_status.value,
|
||||||
company_code = self.company.value,
|
company_code = self.company.value,
|
||||||
auth_key = [0] * 256 # Unsupported
|
auth_key = self.auth_key
|
||||||
))
|
))
|
||||||
|
|
||||||
self.head.length = HEADER_SIZE + len(resp_struct)
|
self.head.length = HEADER_SIZE + len(resp_struct)
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ class ADBLookupExResponse(ADBBaseResponse):
|
|||||||
super().__init__(code, length, status, game_id, store_id, keychip_id)
|
super().__init__(code, length, status, game_id, store_id, keychip_id)
|
||||||
self.user_id = user_id if user_id is not None else -1
|
self.user_id = user_id if user_id is not None else -1
|
||||||
self.portal_reg = PortalRegStatus.NO_REG
|
self.portal_reg = PortalRegStatus.NO_REG
|
||||||
|
self.auth_key = [0] * 256
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_req(cls, req: ADBHeader, user_id: Union[int, None]) -> "ADBLookupExResponse":
|
def from_req(cls, req: ADBHeader, user_id: Union[int, None]) -> "ADBLookupExResponse":
|
||||||
@@ -72,7 +73,7 @@ class ADBLookupExResponse(ADBBaseResponse):
|
|||||||
body = resp_struct.build(dict(
|
body = resp_struct.build(dict(
|
||||||
user_id = self.user_id,
|
user_id = self.user_id,
|
||||||
portal_reg = self.portal_reg.value,
|
portal_reg = self.portal_reg.value,
|
||||||
auth_key = [0] * 256,
|
auth_key = self.auth_key,
|
||||||
relation1 = -1,
|
relation1 = -1,
|
||||||
relation2 = -1
|
relation2 = -1
|
||||||
))
|
))
|
||||||
|
|||||||
228
core/aimedb.py
228
core/aimedb.py
@@ -1,24 +1,45 @@
|
|||||||
from twisted.internet.protocol import Factory, Protocol
|
|
||||||
import logging, coloredlogs
|
import logging, coloredlogs
|
||||||
from Crypto.Cipher import AES
|
from Crypto.Cipher import AES
|
||||||
import struct
|
from typing import Dict, Tuple, Callable, Union, Optional
|
||||||
from typing import Dict, Tuple, Callable, Union
|
import asyncio
|
||||||
from typing_extensions import Final
|
|
||||||
from logging.handlers import TimedRotatingFileHandler
|
from logging.handlers import TimedRotatingFileHandler
|
||||||
|
|
||||||
from core.config import CoreConfig
|
from core.config import CoreConfig
|
||||||
|
from core.utils import create_sega_auth_key
|
||||||
from core.data import Data
|
from core.data import Data
|
||||||
from .adb_handlers import *
|
from .adb_handlers import *
|
||||||
|
|
||||||
|
class AimedbServlette():
|
||||||
class AimedbProtocol(Protocol):
|
|
||||||
request_list: Dict[int, Tuple[Callable[[bytes, int], Union[ADBBaseResponse, bytes]], int, str]] = {}
|
request_list: Dict[int, Tuple[Callable[[bytes, int], Union[ADBBaseResponse, bytes]], int, str]] = {}
|
||||||
|
|
||||||
def __init__(self, core_cfg: CoreConfig) -> None:
|
def __init__(self, core_cfg: CoreConfig) -> None:
|
||||||
self.logger = logging.getLogger("aimedb")
|
|
||||||
self.config = core_cfg
|
self.config = core_cfg
|
||||||
self.data = Data(core_cfg)
|
self.data = Data(core_cfg)
|
||||||
if core_cfg.aimedb.key == "":
|
|
||||||
|
self.logger = logging.getLogger("aimedb")
|
||||||
|
if not hasattr(self.logger, "initted"):
|
||||||
|
log_fmt_str = "[%(asctime)s] Aimedb | %(levelname)s | %(message)s"
|
||||||
|
log_fmt = logging.Formatter(log_fmt_str)
|
||||||
|
|
||||||
|
fileHandler = TimedRotatingFileHandler(
|
||||||
|
"{0}/{1}.log".format(self.config.server.log_dir, "aimedb"),
|
||||||
|
when="d",
|
||||||
|
backupCount=10,
|
||||||
|
)
|
||||||
|
fileHandler.setFormatter(log_fmt)
|
||||||
|
|
||||||
|
consoleHandler = logging.StreamHandler()
|
||||||
|
consoleHandler.setFormatter(log_fmt)
|
||||||
|
|
||||||
|
self.logger.addHandler(fileHandler)
|
||||||
|
self.logger.addHandler(consoleHandler)
|
||||||
|
|
||||||
|
self.logger.setLevel(self.config.aimedb.loglevel)
|
||||||
|
coloredlogs.install(
|
||||||
|
level=core_cfg.aimedb.loglevel, logger=self.logger, fmt=log_fmt_str
|
||||||
|
)
|
||||||
|
self.logger.initted = True
|
||||||
|
|
||||||
|
if not core_cfg.aimedb.key:
|
||||||
self.logger.error("!!!KEY NOT SET!!!")
|
self.logger.error("!!!KEY NOT SET!!!")
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
@@ -39,27 +60,31 @@ class AimedbProtocol(Protocol):
|
|||||||
|
|
||||||
self.register_handler(0x13, 0x14, self.handle_log_ex, 'aime_log_ex')
|
self.register_handler(0x13, 0x14, self.handle_log_ex, 'aime_log_ex')
|
||||||
self.register_handler(0x64, 0x65, self.handle_hello, 'hello')
|
self.register_handler(0x64, 0x65, self.handle_hello, 'hello')
|
||||||
self.register_handler(0x66, 0, self.handle_goodbye, 'goodbye')
|
|
||||||
|
|
||||||
def register_handler(self, cmd: int, resp:int, handler: Callable[[bytes, int], Union[ADBBaseResponse, bytes]], name: str) -> None:
|
def register_handler(self, cmd: int, resp:int, handler: Callable[[bytes, int], Union[ADBBaseResponse, bytes]], name: str) -> None:
|
||||||
self.request_list[cmd] = (handler, resp, name)
|
self.request_list[cmd] = (handler, resp, name)
|
||||||
|
|
||||||
def append_padding(self, data: bytes):
|
def start(self) -> None:
|
||||||
"""Appends 0s to the end of the data until it's at the correct size"""
|
self.logger.info(f"Start on port {self.config.aimedb.port}")
|
||||||
length = struct.unpack_from("<H", data, 6)
|
addr = self.config.aimedb.listen_address if self.config.aimedb.listen_address else self.config.server.listen_address
|
||||||
padding_size = length[0] - len(data)
|
asyncio.create_task(asyncio.start_server(self.dataReceived, addr, self.config.aimedb.port))
|
||||||
data += bytes(padding_size)
|
|
||||||
return data
|
|
||||||
|
|
||||||
def connectionMade(self) -> None:
|
async def dataReceived(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
|
||||||
self.logger.debug(f"{self.transport.getPeer().host} Connected")
|
self.logger.debug(f"Connection made from {writer.get_extra_info('peername')[0]}")
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
data: bytes = await reader.read(4096)
|
||||||
|
if len(data) == 0:
|
||||||
|
self.logger.debug("Connection closed")
|
||||||
|
return
|
||||||
|
await self.process_data(data, reader, writer)
|
||||||
|
await writer.drain()
|
||||||
|
except ConnectionResetError as e:
|
||||||
|
self.logger.debug("Connection reset, disconnecting")
|
||||||
|
return
|
||||||
|
|
||||||
def connectionLost(self, reason) -> None:
|
async def process_data(self, data: bytes, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> Optional[bytes]:
|
||||||
self.logger.debug(
|
addr = writer.get_extra_info('peername')[0]
|
||||||
f"{self.transport.getPeer().host} Disconnected - {reason.value}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def dataReceived(self, data: bytes) -> None:
|
|
||||||
cipher = AES.new(self.config.aimedb.key.encode(), AES.MODE_ECB)
|
cipher = AES.new(self.config.aimedb.key.encode(), AES.MODE_ECB)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -67,9 +92,9 @@ class AimedbProtocol(Protocol):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Failed to decrypt {data.hex()} because {e}")
|
self.logger.error(f"Failed to decrypt {data.hex()} because {e}")
|
||||||
return None
|
return
|
||||||
|
|
||||||
self.logger.debug(f"{self.transport.getPeer().host} wrote {decrypted.hex()}")
|
self.logger.debug(f"{addr} wrote {decrypted.hex()}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
head = ADBHeader.from_data(decrypted)
|
head = ADBHeader.from_data(decrypted)
|
||||||
@@ -78,7 +103,9 @@ class AimedbProtocol(Protocol):
|
|||||||
self.logger.error(f"Error parsing ADB header: {e}")
|
self.logger.error(f"Error parsing ADB header: {e}")
|
||||||
try:
|
try:
|
||||||
encrypted = cipher.encrypt(ADBBaseResponse().make())
|
encrypted = cipher.encrypt(ADBBaseResponse().make())
|
||||||
self.transport.write(encrypted)
|
writer.write(encrypted)
|
||||||
|
await writer.drain()
|
||||||
|
return
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Failed to encrypt default response because {e}")
|
self.logger.error(f"Failed to encrypt default response because {e}")
|
||||||
@@ -88,46 +115,51 @@ class AimedbProtocol(Protocol):
|
|||||||
if head.keychip_id == "ABCD1234567" or head.store_id == 0xfff0:
|
if head.keychip_id == "ABCD1234567" or head.store_id == 0xfff0:
|
||||||
self.logger.warning(f"Request from uninitialized AMLib: {vars(head)}")
|
self.logger.warning(f"Request from uninitialized AMLib: {vars(head)}")
|
||||||
|
|
||||||
|
if head.cmd == 0x66:
|
||||||
|
self.logger.info("Goodbye")
|
||||||
|
writer.close()
|
||||||
|
return
|
||||||
|
|
||||||
handler, resp_code, name = self.request_list.get(head.cmd, (self.handle_default, None, 'default'))
|
handler, resp_code, name = self.request_list.get(head.cmd, (self.handle_default, None, 'default'))
|
||||||
|
|
||||||
if resp_code is None:
|
if resp_code is None:
|
||||||
self.logger.warning(f"No handler for cmd {hex(head.cmd)}")
|
self.logger.warning(f"No handler for cmd {hex(head.cmd)}")
|
||||||
|
|
||||||
elif resp_code > 0:
|
elif resp_code > 0:
|
||||||
self.logger.info(f"{name} from {head.keychip_id} ({head.game_id}) @ {self.transport.getPeer().host}")
|
self.logger.info(f"{name} from {head.keychip_id} ({head.game_id}) @ {addr}")
|
||||||
|
|
||||||
resp = handler(decrypted, resp_code)
|
resp = await handler(decrypted, resp_code)
|
||||||
|
|
||||||
if type(resp) == ADBBaseResponse or issubclass(type(resp), ADBBaseResponse):
|
if type(resp) == ADBBaseResponse or issubclass(type(resp), ADBBaseResponse):
|
||||||
resp_bytes = resp.make()
|
resp_bytes = resp.make()
|
||||||
if len(resp_bytes) != resp.head.length:
|
|
||||||
resp_bytes = self.append_padding(resp_bytes)
|
|
||||||
|
|
||||||
elif type(resp) == bytes:
|
elif type(resp) == bytes:
|
||||||
resp_bytes = resp
|
resp_bytes = resp
|
||||||
|
|
||||||
elif resp is None: # Nothing to send, probably a goodbye
|
elif resp is None: # Nothing to send, probably a goodbye
|
||||||
|
self.logger.warn(f"None return by handler for {name}")
|
||||||
return
|
return
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
self.logger.error(f"Unsupported type returned by ADB handler for {name}: {type(resp)}")
|
||||||
raise TypeError(f"Unsupported type returned by ADB handler for {name}: {type(resp)}")
|
raise TypeError(f"Unsupported type returned by ADB handler for {name}: {type(resp)}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
encrypted = cipher.encrypt(resp_bytes)
|
encrypted = cipher.encrypt(resp_bytes)
|
||||||
self.logger.debug(f"Response {resp_bytes.hex()}")
|
self.logger.debug(f"Response {resp_bytes.hex()}")
|
||||||
self.transport.write(encrypted)
|
writer.write(encrypted)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Failed to encrypt {resp_bytes.hex()} because {e}")
|
self.logger.error(f"Failed to encrypt {resp_bytes.hex()} because {e}")
|
||||||
|
|
||||||
def handle_default(self, data: bytes, resp_code: int, length: int = 0x20) -> ADBBaseResponse:
|
async def handle_default(self, data: bytes, resp_code: int, length: int = 0x20) -> ADBBaseResponse:
|
||||||
req = ADBHeader.from_data(data)
|
req = ADBHeader.from_data(data)
|
||||||
return ADBBaseResponse(resp_code, length, 1, req.game_id, req.store_id, req.keychip_id, req.protocol_ver)
|
return ADBBaseResponse(resp_code, length, 1, req.game_id, req.store_id, req.keychip_id, req.protocol_ver)
|
||||||
|
|
||||||
def handle_hello(self, data: bytes, resp_code: int) -> ADBBaseResponse:
|
async def handle_hello(self, data: bytes, resp_code: int) -> ADBBaseResponse:
|
||||||
return self.handle_default(data, resp_code)
|
return await self.handle_default(data, resp_code)
|
||||||
|
|
||||||
def handle_campaign(self, data: bytes, resp_code: int) -> ADBBaseResponse:
|
async def handle_campaign(self, data: bytes, resp_code: int) -> ADBBaseResponse:
|
||||||
h = ADBHeader.from_data(data)
|
h = ADBHeader.from_data(data)
|
||||||
if h.protocol_ver >= 0x3030:
|
if h.protocol_ver >= 0x3030:
|
||||||
req = h
|
req = h
|
||||||
@@ -142,29 +174,60 @@ class AimedbProtocol(Protocol):
|
|||||||
# We don't currently support campaigns
|
# We don't currently support campaigns
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
def handle_lookup(self, data: bytes, resp_code: int) -> ADBBaseResponse:
|
async def handle_lookup(self, data: bytes, resp_code: int) -> ADBBaseResponse:
|
||||||
req = ADBLookupRequest(data)
|
req = ADBLookupRequest(data)
|
||||||
user_id = self.data.card.get_user_id_from_card(req.access_code)
|
user_id = await self.data.card.get_user_id_from_card(req.access_code)
|
||||||
|
is_banned = await self.data.card.get_card_banned(req.access_code)
|
||||||
|
is_locked = await self.data.card.get_card_locked(req.access_code)
|
||||||
|
|
||||||
ret = ADBLookupResponse.from_req(req.head, user_id)
|
ret = ADBLookupResponse.from_req(req.head, user_id)
|
||||||
|
if is_banned and is_locked:
|
||||||
|
ret.head.status = ADBStatus.BAN_SYS_USER
|
||||||
|
elif is_banned:
|
||||||
|
ret.head.status = ADBStatus.BAN_SYS
|
||||||
|
elif is_locked:
|
||||||
|
ret.head.status = ADBStatus.LOCK_USER
|
||||||
|
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
f"access_code {req.access_code} -> user_id {ret.user_id}"
|
f"access_code {req.access_code} -> user_id {ret.user_id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if user_id and user_id > 0:
|
||||||
|
await self.data.card.update_card_last_login(req.access_code)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def handle_lookup_ex(self, data: bytes, resp_code: int) -> ADBBaseResponse:
|
async def handle_lookup_ex(self, data: bytes, resp_code: int) -> ADBBaseResponse:
|
||||||
req = ADBLookupRequest(data)
|
req = ADBLookupRequest(data)
|
||||||
user_id = self.data.card.get_user_id_from_card(req.access_code)
|
user_id = await self.data.card.get_user_id_from_card(req.access_code)
|
||||||
|
|
||||||
|
is_banned = await self.data.card.get_card_banned(req.access_code)
|
||||||
|
is_locked = await self.data.card.get_card_locked(req.access_code)
|
||||||
|
|
||||||
ret = ADBLookupExResponse.from_req(req.head, user_id)
|
ret = ADBLookupExResponse.from_req(req.head, user_id)
|
||||||
|
if is_banned and is_locked:
|
||||||
|
ret.head.status = ADBStatus.BAN_SYS_USER
|
||||||
|
elif is_banned:
|
||||||
|
ret.head.status = ADBStatus.BAN_SYS
|
||||||
|
elif is_locked:
|
||||||
|
ret.head.status = ADBStatus.LOCK_USER
|
||||||
|
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
f"access_code {req.access_code} -> user_id {ret.user_id}"
|
f"access_code {req.access_code} -> user_id {ret.user_id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if user_id and user_id > 0 and self.config.aimedb.id_secret:
|
||||||
|
auth_key = create_sega_auth_key(user_id, req.head.game_id, req.head.store_id, req.head.keychip_id, self.config.aimedb.id_secret, self.config.aimedb.id_lifetime_seconds)
|
||||||
|
if auth_key is not None:
|
||||||
|
auth_key_extra_len = 256 - len(auth_key)
|
||||||
|
auth_key_full = auth_key.encode() + (b"\0" * auth_key_extra_len)
|
||||||
|
self.logger.debug(f"Generated auth token {auth_key}")
|
||||||
|
ret.auth_key = auth_key_full
|
||||||
|
|
||||||
|
if user_id and user_id > 0:
|
||||||
|
await self.data.card.update_card_last_login(req.access_code)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def handle_felica_lookup(self, data: bytes, resp_code: int) -> bytes:
|
async def handle_felica_lookup(self, data: bytes, resp_code: int) -> bytes:
|
||||||
"""
|
"""
|
||||||
On official, I think a card has to be registered for this to actually work, but
|
On official, I think a card has to be registered for this to actually work, but
|
||||||
I'm making the executive decision to not implement that and just kick back our
|
I'm making the executive decision to not implement that and just kick back our
|
||||||
@@ -180,7 +243,7 @@ class AimedbProtocol(Protocol):
|
|||||||
)
|
)
|
||||||
return ADBFelicaLookupResponse.from_req(req.head, ac)
|
return ADBFelicaLookupResponse.from_req(req.head, ac)
|
||||||
|
|
||||||
def handle_felica_register(self, data: bytes, resp_code: int) -> bytes:
|
async def handle_felica_register(self, data: bytes, resp_code: int) -> bytes:
|
||||||
"""
|
"""
|
||||||
I've never seen this used.
|
I've never seen this used.
|
||||||
"""
|
"""
|
||||||
@@ -188,14 +251,14 @@ class AimedbProtocol(Protocol):
|
|||||||
ac = self.data.card.to_access_code(req.idm)
|
ac = self.data.card.to_access_code(req.idm)
|
||||||
|
|
||||||
if self.config.server.allow_user_registration:
|
if self.config.server.allow_user_registration:
|
||||||
user_id = self.data.user.create_user()
|
user_id = await self.data.user.create_user()
|
||||||
|
|
||||||
if user_id is None:
|
if user_id is None:
|
||||||
self.logger.error("Failed to register user!")
|
self.logger.error("Failed to register user!")
|
||||||
user_id = -1
|
user_id = -1
|
||||||
|
|
||||||
else:
|
else:
|
||||||
card_id = self.data.card.create_card(user_id, ac)
|
card_id = await self.data.card.create_card(user_id, ac)
|
||||||
|
|
||||||
if card_id is None:
|
if card_id is None:
|
||||||
self.logger.error("Failed to register card!")
|
self.logger.error("Failed to register card!")
|
||||||
@@ -210,12 +273,14 @@ class AimedbProtocol(Protocol):
|
|||||||
f"Registration blocked!: access code {ac} (IDm: {req.idm} PMm: {req.pmm})"
|
f"Registration blocked!: access code {ac} (IDm: {req.idm} PMm: {req.pmm})"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if user_id > 0:
|
||||||
|
await self.data.card.update_card_last_login(ac)
|
||||||
return ADBFelicaLookupResponse.from_req(req.head, ac)
|
return ADBFelicaLookupResponse.from_req(req.head, ac)
|
||||||
|
|
||||||
def handle_felica_lookup_ex(self, data: bytes, resp_code: int) -> bytes:
|
async def handle_felica_lookup_ex(self, data: bytes, resp_code: int) -> bytes:
|
||||||
req = ADBFelicaLookup2Request(data)
|
req = ADBFelicaLookup2Request(data)
|
||||||
access_code = self.data.card.to_access_code(req.idm)
|
access_code = self.data.card.to_access_code(req.idm)
|
||||||
user_id = self.data.card.get_user_id_from_card(access_code=access_code)
|
user_id = await self.data.card.get_user_id_from_card(access_code=access_code)
|
||||||
|
|
||||||
if user_id is None:
|
if user_id is None:
|
||||||
user_id = -1
|
user_id = -1
|
||||||
@@ -224,9 +289,21 @@ class AimedbProtocol(Protocol):
|
|||||||
f"idm {req.idm} ipm {req.pmm} -> access_code {access_code} user_id {user_id}"
|
f"idm {req.idm} ipm {req.pmm} -> access_code {access_code} user_id {user_id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
return ADBFelicaLookup2Response.from_req(req.head, user_id, access_code)
|
resp = ADBFelicaLookup2Response.from_req(req.head, user_id, access_code)
|
||||||
|
|
||||||
def handle_campaign_clear(self, data: bytes, resp_code: int) -> ADBBaseResponse:
|
if user_id and user_id > 0 and self.config.aimedb.id_secret:
|
||||||
|
auth_key = create_sega_auth_key(user_id, req.head.game_id, req.head.store_id, req.head.keychip_id, self.config.aimedb.id_secret, self.config.aimedb.id_lifetime_seconds)
|
||||||
|
if auth_key is not None:
|
||||||
|
auth_key_extra_len = 256 - len(auth_key)
|
||||||
|
auth_key_full = auth_key.encode() + (b"\0" * auth_key_extra_len)
|
||||||
|
self.logger.debug(f"Generated auth token {auth_key}")
|
||||||
|
resp.auth_key = auth_key_full
|
||||||
|
|
||||||
|
if user_id and user_id > 0:
|
||||||
|
await self.data.card.update_card_last_login(access_code)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
async def handle_campaign_clear(self, data: bytes, resp_code: int) -> ADBBaseResponse:
|
||||||
req = ADBCampaignClearRequest(data)
|
req = ADBCampaignClearRequest(data)
|
||||||
|
|
||||||
resp = ADBCampaignClearResponse.from_req(req.head)
|
resp = ADBCampaignClearResponse.from_req(req.head)
|
||||||
@@ -234,19 +311,19 @@ class AimedbProtocol(Protocol):
|
|||||||
# We don't support campaign stuff
|
# We don't support campaign stuff
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
def handle_register(self, data: bytes, resp_code: int) -> bytes:
|
async def handle_register(self, data: bytes, resp_code: int) -> bytes:
|
||||||
req = ADBLookupRequest(data)
|
req = ADBLookupRequest(data)
|
||||||
user_id = -1
|
user_id = -1
|
||||||
|
|
||||||
if self.config.server.allow_user_registration:
|
if self.config.server.allow_user_registration:
|
||||||
user_id = self.data.user.create_user()
|
user_id = await self.data.user.create_user()
|
||||||
|
|
||||||
if user_id is None:
|
if user_id is None:
|
||||||
self.logger.error("Failed to register user!")
|
self.logger.error("Failed to register user!")
|
||||||
user_id = -1
|
user_id = -1
|
||||||
|
|
||||||
else:
|
else:
|
||||||
card_id = self.data.card.create_card(user_id, req.access_code)
|
card_id = await self.data.card.create_card(user_id, req.access_code)
|
||||||
|
|
||||||
if card_id is None:
|
if card_id is None:
|
||||||
self.logger.error("Failed to register card!")
|
self.logger.error("Failed to register card!")
|
||||||
@@ -265,20 +342,23 @@ class AimedbProtocol(Protocol):
|
|||||||
if resp.user_id <= 0:
|
if resp.user_id <= 0:
|
||||||
resp.head.status = ADBStatus.BAN_SYS # Closest we can get to a "You cannot register"
|
resp.head.status = ADBStatus.BAN_SYS # Closest we can get to a "You cannot register"
|
||||||
|
|
||||||
|
else:
|
||||||
|
await self.data.card.update_card_last_login(req.access_code)
|
||||||
|
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
# TODO: Save these in some capacity, as deemed relevant
|
# TODO: Save these in some capacity, as deemed relevant
|
||||||
def handle_status_log(self, data: bytes, resp_code: int) -> bytes:
|
async def handle_status_log(self, data: bytes, resp_code: int) -> bytes:
|
||||||
req = ADBStatusLogRequest(data)
|
req = ADBStatusLogRequest(data)
|
||||||
self.logger.info(f"User {req.aime_id} logged {req.status.name} event")
|
self.logger.info(f"User {req.aime_id} logged {req.status.name} event")
|
||||||
return ADBBaseResponse(resp_code, 0x20, 1, req.head.game_id, req.head.store_id, req.head.keychip_id, req.head.protocol_ver)
|
return ADBBaseResponse(resp_code, 0x20, 1, req.head.game_id, req.head.store_id, req.head.keychip_id, req.head.protocol_ver)
|
||||||
|
|
||||||
def handle_log(self, data: bytes, resp_code: int) -> bytes:
|
async def handle_log(self, data: bytes, resp_code: int) -> bytes:
|
||||||
req = ADBLogRequest(data)
|
req = ADBLogRequest(data)
|
||||||
self.logger.info(f"User {req.aime_id} logged {req.status.name} event, credit_ct: {req.credit_ct} bet_ct: {req.bet_ct} won_ct: {req.won_ct}")
|
self.logger.info(f"User {req.aime_id} logged {req.status.name} event, credit_ct: {req.credit_ct} bet_ct: {req.bet_ct} won_ct: {req.won_ct}")
|
||||||
return ADBBaseResponse(resp_code, 0x20, 1, req.head.game_id, req.head.store_id, req.head.keychip_id, req.head.protocol_ver)
|
return ADBBaseResponse(resp_code, 0x20, 1, req.head.game_id, req.head.store_id, req.head.keychip_id, req.head.protocol_ver)
|
||||||
|
|
||||||
def handle_log_ex(self, data: bytes, resp_code: int) -> bytes:
|
async def handle_log_ex(self, data: bytes, resp_code: int) -> bytes:
|
||||||
req = ADBLogExRequest(data)
|
req = ADBLogExRequest(data)
|
||||||
strs = []
|
strs = []
|
||||||
self.logger.info(f"Recieved {req.num_logs} or {len(req.logs)} logs")
|
self.logger.info(f"Recieved {req.num_logs} or {len(req.logs)} logs")
|
||||||
@@ -287,43 +367,3 @@ class AimedbProtocol(Protocol):
|
|||||||
self.logger.debug(f"User {req.logs[x].aime_id} logged {req.logs[x].status.name} event, credit_ct: {req.logs[x].credit_ct} bet_ct: {req.logs[x].bet_ct} won_ct: {req.logs[x].won_ct}")
|
self.logger.debug(f"User {req.logs[x].aime_id} logged {req.logs[x].status.name} event, credit_ct: {req.logs[x].credit_ct} bet_ct: {req.logs[x].bet_ct} won_ct: {req.logs[x].won_ct}")
|
||||||
return ADBLogExResponse.from_req(req.head)
|
return ADBLogExResponse.from_req(req.head)
|
||||||
|
|
||||||
def handle_goodbye(self, data: bytes, resp_code: int) -> None:
|
|
||||||
self.logger.info(f"goodbye from {self.transport.getPeer().host}")
|
|
||||||
self.transport.loseConnection()
|
|
||||||
return
|
|
||||||
|
|
||||||
class AimedbFactory(Factory):
|
|
||||||
protocol = AimedbProtocol
|
|
||||||
|
|
||||||
def __init__(self, cfg: CoreConfig) -> None:
|
|
||||||
self.config = cfg
|
|
||||||
log_fmt_str = "[%(asctime)s] Aimedb | %(levelname)s | %(message)s"
|
|
||||||
log_fmt = logging.Formatter(log_fmt_str)
|
|
||||||
self.logger = logging.getLogger("aimedb")
|
|
||||||
|
|
||||||
fileHandler = TimedRotatingFileHandler(
|
|
||||||
"{0}/{1}.log".format(self.config.server.log_dir, "aimedb"),
|
|
||||||
when="d",
|
|
||||||
backupCount=10,
|
|
||||||
)
|
|
||||||
fileHandler.setFormatter(log_fmt)
|
|
||||||
|
|
||||||
consoleHandler = logging.StreamHandler()
|
|
||||||
consoleHandler.setFormatter(log_fmt)
|
|
||||||
|
|
||||||
self.logger.addHandler(fileHandler)
|
|
||||||
self.logger.addHandler(consoleHandler)
|
|
||||||
|
|
||||||
self.logger.setLevel(self.config.aimedb.loglevel)
|
|
||||||
coloredlogs.install(
|
|
||||||
level=cfg.aimedb.loglevel, logger=self.logger, fmt=log_fmt_str
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.config.aimedb.key == "":
|
|
||||||
self.logger.error("Please set 'key' field in your config file.")
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
self.logger.info(f"Ready on port {self.config.aimedb.port}")
|
|
||||||
|
|
||||||
def buildProtocol(self, addr):
|
|
||||||
return AimedbProtocol(self.config)
|
|
||||||
|
|||||||
614
core/allnet.py
614
core/allnet.py
@@ -1,25 +1,30 @@
|
|||||||
from typing import Dict, List, Any, Optional, Tuple, Union, Final
|
|
||||||
import logging, coloredlogs
|
|
||||||
from logging.handlers import TimedRotatingFileHandler
|
|
||||||
from twisted.web.http import Request
|
|
||||||
from datetime import datetime
|
|
||||||
import pytz
|
import pytz
|
||||||
import base64
|
import base64
|
||||||
import zlib
|
import zlib
|
||||||
import json
|
import json
|
||||||
|
import yaml
|
||||||
|
import logging
|
||||||
|
import coloredlogs
|
||||||
|
import urllib.parse
|
||||||
|
import math
|
||||||
|
from typing import Dict, List, Any, Optional, Union, Final
|
||||||
|
from logging.handlers import TimedRotatingFileHandler
|
||||||
|
from starlette.requests import Request
|
||||||
|
from starlette.responses import PlainTextResponse
|
||||||
|
from starlette.applications import Starlette
|
||||||
|
from starlette.routing import Route
|
||||||
|
from datetime import datetime
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from Crypto.PublicKey import RSA
|
from Crypto.PublicKey import RSA
|
||||||
from Crypto.Hash import SHA
|
from Crypto.Hash import SHA
|
||||||
from Crypto.Signature import PKCS1_v1_5
|
from Crypto.Signature import PKCS1_v1_5
|
||||||
from time import strptime
|
from os import path, environ, mkdir, access, W_OK
|
||||||
from os import path
|
|
||||||
import urllib.parse
|
|
||||||
import math
|
|
||||||
|
|
||||||
from core.config import CoreConfig
|
from .config import CoreConfig
|
||||||
from core.utils import Utils
|
from .utils import Utils
|
||||||
from core.data import Data
|
from .data import Data
|
||||||
from core.const import *
|
from .const import *
|
||||||
|
from .title import TitleServlet
|
||||||
|
|
||||||
BILLING_DT_FORMAT: Final[str] = "%Y%m%d%H%M%S"
|
BILLING_DT_FORMAT: Final[str] = "%Y%m%d%H%M%S"
|
||||||
|
|
||||||
@@ -33,13 +38,67 @@ class ALLNET_STAT(Enum):
|
|||||||
bad_machine = -2
|
bad_machine = -2
|
||||||
bad_shop = -3
|
bad_shop = -3
|
||||||
|
|
||||||
|
class DLI_STATUS(Enum):
|
||||||
|
START = 0
|
||||||
|
GET_DOWNLOAD_CONFIGURATION = 1
|
||||||
|
WAIT_DOWNLOAD = 2
|
||||||
|
DOWNLOADING = 3
|
||||||
|
|
||||||
|
NOT_SPECIFY_DLI = 100
|
||||||
|
ONLY_POST_REPORT = 101
|
||||||
|
STOPPED_BY_APP_RELEASE = 102
|
||||||
|
STOPPED_BY_OPT_RELEASE = 103
|
||||||
|
|
||||||
|
DOWNLOAD_COMPLETE_RECENTLY = 110
|
||||||
|
|
||||||
|
DOWNLOAD_COMPLETE_WAIT_RELEASE_TIME = 120
|
||||||
|
DOWNLOAD_COMPLETE_BUT_NOT_SYNC_SERVER = 121
|
||||||
|
DOWNLOAD_COMPLETE_BUT_NOT_FIRST_RESUME = 122
|
||||||
|
DOWNLOAD_COMPLETE_BUT_NOT_FIRST_LAUNCH = 123
|
||||||
|
DOWNLOAD_COMPLETE_WAIT_UPDATE = 124
|
||||||
|
|
||||||
|
DOWNLOAD_COMPLETE_AND_ALREADY_UPDATE = 130
|
||||||
|
|
||||||
|
ERROR_AUTH_FAILURE = 200
|
||||||
|
|
||||||
|
ERROR_GET_DLI_HTTP = 300
|
||||||
|
ERROR_GET_DLI = 301
|
||||||
|
ERROR_PARSE_DLI = 302
|
||||||
|
ERROR_INVALID_GAME_ID = 303
|
||||||
|
ERROR_INVALID_IMAGE_LIST = 304
|
||||||
|
ERROR_GET_DLI_APP = 305
|
||||||
|
|
||||||
|
ERROR_GET_BOOT_ID = 400
|
||||||
|
ERROR_ACCESS_SERVER = 401
|
||||||
|
ERROR_NO_IMAGE = 402
|
||||||
|
ERROR_ACCESS_IMAGE = 403
|
||||||
|
|
||||||
|
ERROR_DOWNLOAD_APP = 500
|
||||||
|
ERROR_DOWNLOAD_OPT = 501
|
||||||
|
|
||||||
|
ERROR_DISK_FULL = 600
|
||||||
|
ERROR_UNINSTALL = 601
|
||||||
|
ERROR_INSTALL_APP = 602
|
||||||
|
ERROR_INSTALL_OPT = 603
|
||||||
|
|
||||||
|
ERROR_GET_DLI_INTERNAL = 900
|
||||||
|
ERROR_ICF = 901
|
||||||
|
ERROR_CHECK_RELEASE_INTERNAL = 902
|
||||||
|
UNKNOWN = 999 # Not the actual enum val but it needs to be here as a catch-all
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_int(cls, num: int) -> "DLI_STATUS":
|
||||||
|
try:
|
||||||
|
return cls(num)
|
||||||
|
except ValueError:
|
||||||
|
return cls.UNKNOWN
|
||||||
|
|
||||||
class AllnetServlet:
|
class AllnetServlet:
|
||||||
|
allnet_registry: Dict[str, Any] = {}
|
||||||
def __init__(self, core_cfg: CoreConfig, cfg_folder: str):
|
def __init__(self, core_cfg: CoreConfig, cfg_folder: str):
|
||||||
super().__init__()
|
|
||||||
self.config = core_cfg
|
self.config = core_cfg
|
||||||
self.config_folder = cfg_folder
|
self.config_folder = cfg_folder
|
||||||
self.data = Data(core_cfg)
|
self.data = Data(core_cfg)
|
||||||
self.uri_registry: Dict[str, Tuple[str, str]] = {}
|
|
||||||
|
|
||||||
self.logger = logging.getLogger("allnet")
|
self.logger = logging.getLogger("allnet")
|
||||||
if not hasattr(self.logger, "initialized"):
|
if not hasattr(self.logger, "initialized"):
|
||||||
@@ -65,35 +124,22 @@ class AllnetServlet:
|
|||||||
)
|
)
|
||||||
self.logger.initialized = True
|
self.logger.initialized = True
|
||||||
|
|
||||||
plugins = Utils.get_all_titles()
|
def startup(self) -> None:
|
||||||
|
self.logger.info(f"Ready on port {self.config.allnet.port if self.config.allnet.standalone else self.config.server.port}")
|
||||||
|
if not TitleServlet.title_registry:
|
||||||
|
TitleServlet(self.config, self.config_folder)
|
||||||
|
|
||||||
if len(plugins) == 0:
|
async def handle_poweron(self, request: Request):
|
||||||
self.logger.error("No games detected!")
|
|
||||||
|
|
||||||
for _, mod in plugins.items():
|
|
||||||
if hasattr(mod, "index") and hasattr(mod.index, "get_allnet_info"):
|
|
||||||
for code in mod.game_codes:
|
|
||||||
enabled, uri, host = mod.index.get_allnet_info(
|
|
||||||
code, self.config, self.config_folder
|
|
||||||
)
|
|
||||||
|
|
||||||
if enabled:
|
|
||||||
self.uri_registry[code] = (uri, host)
|
|
||||||
|
|
||||||
self.logger.info(
|
|
||||||
f"Serving {len(self.uri_registry)} game codes port {core_cfg.allnet.port}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def handle_poweron(self, request: Request, _: Dict):
|
|
||||||
request_ip = Utils.get_ip_addr(request)
|
request_ip = Utils.get_ip_addr(request)
|
||||||
pragma_header = request.getHeader('Pragma')
|
pragma_header = request.headers.get('Pragma', "")
|
||||||
is_dfi = pragma_header is not None and pragma_header == "DFI"
|
is_dfi = pragma_header is not None and pragma_header == "DFI"
|
||||||
|
data = await request.body()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if is_dfi:
|
if is_dfi:
|
||||||
req_urlencode = self.from_dfi(request.content.getvalue())
|
req_urlencode = self.from_dfi(data)
|
||||||
else:
|
else:
|
||||||
req_urlencode = request.content.getvalue().decode()
|
req_urlencode = data
|
||||||
|
|
||||||
req_dict = self.allnet_req_to_dict(req_urlencode)
|
req_dict = self.allnet_req_to_dict(req_urlencode)
|
||||||
if req_dict is None:
|
if req_dict is None:
|
||||||
@@ -110,7 +156,7 @@ class AllnetServlet:
|
|||||||
except AllnetRequestException as e:
|
except AllnetRequestException as e:
|
||||||
if e.message != "":
|
if e.message != "":
|
||||||
self.logger.error(e)
|
self.logger.error(e)
|
||||||
return b""
|
return PlainTextResponse()
|
||||||
|
|
||||||
if req.format_ver == 3:
|
if req.format_ver == 3:
|
||||||
resp = AllnetPowerOnResponse3(req.token)
|
resp = AllnetPowerOnResponse3(req.token)
|
||||||
@@ -121,42 +167,42 @@ class AllnetServlet:
|
|||||||
|
|
||||||
self.logger.debug(f"Allnet request: {vars(req)}")
|
self.logger.debug(f"Allnet request: {vars(req)}")
|
||||||
|
|
||||||
machine = self.data.arcade.get_machine(req.serial)
|
machine = await self.data.arcade.get_machine(req.serial)
|
||||||
if machine is None and not self.config.server.allow_unregistered_serials:
|
if machine is None and not self.config.server.allow_unregistered_serials:
|
||||||
msg = f"Unrecognised serial {req.serial} attempted allnet auth from {request_ip}."
|
msg = f"Unrecognised serial {req.serial} attempted allnet auth from {request_ip}."
|
||||||
self.data.base.log_event(
|
await self.data.base.log_event(
|
||||||
"allnet", "ALLNET_AUTH_UNKNOWN_SERIAL", logging.WARN, msg
|
"allnet", "ALLNET_AUTH_UNKNOWN_SERIAL", logging.WARN, msg
|
||||||
)
|
)
|
||||||
self.logger.warning(msg)
|
self.logger.warning(msg)
|
||||||
|
|
||||||
resp.stat = ALLNET_STAT.bad_machine.value
|
resp.stat = ALLNET_STAT.bad_machine.value
|
||||||
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
|
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
|
||||||
return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8")
|
return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n")
|
||||||
|
|
||||||
if machine is not None:
|
if machine is not None:
|
||||||
arcade = self.data.arcade.get_arcade(machine["arcade"])
|
arcade = await self.data.arcade.get_arcade(machine["arcade"])
|
||||||
if self.config.server.check_arcade_ip:
|
if self.config.server.check_arcade_ip:
|
||||||
if arcade["ip"] and arcade["ip"] is not None and arcade["ip"] != req.ip:
|
if arcade["ip"] and arcade["ip"] is not None and arcade["ip"] != req.ip:
|
||||||
msg = f"Serial {req.serial} attempted allnet auth from bad IP {req.ip} (expected {arcade['ip']})."
|
msg = f"Serial {req.serial} attempted allnet auth from bad IP {req.ip} (expected {arcade['ip']})."
|
||||||
self.data.base.log_event(
|
await self.data.base.log_event(
|
||||||
"allnet", "ALLNET_AUTH_BAD_IP", logging.ERROR, msg
|
"allnet", "ALLNET_AUTH_BAD_IP", logging.ERROR, msg
|
||||||
)
|
)
|
||||||
self.logger.warning(msg)
|
self.logger.warning(msg)
|
||||||
|
|
||||||
resp.stat = ALLNET_STAT.bad_shop.value
|
resp.stat = ALLNET_STAT.bad_shop.value
|
||||||
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
|
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
|
||||||
return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8")
|
return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n")
|
||||||
|
|
||||||
elif not arcade["ip"] or arcade["ip"] is None and self.config.server.strict_ip_checking:
|
elif (not arcade["ip"] or arcade["ip"] is None) and self.config.server.strict_ip_checking:
|
||||||
msg = f"Serial {req.serial} attempted allnet auth from bad IP {req.ip}, but arcade {arcade['id']} has no IP set! (strict checking enabled)."
|
msg = f"Serial {req.serial} attempted allnet auth from bad IP {req.ip}, but arcade {arcade['id']} has no IP set! (strict checking enabled)."
|
||||||
self.data.base.log_event(
|
await self.data.base.log_event(
|
||||||
"allnet", "ALLNET_AUTH_NO_SHOP_IP", logging.ERROR, msg
|
"allnet", "ALLNET_AUTH_NO_SHOP_IP", logging.ERROR, msg
|
||||||
)
|
)
|
||||||
self.logger.warning(msg)
|
self.logger.warning(msg)
|
||||||
|
|
||||||
resp.stat = ALLNET_STAT.bad_shop.value
|
resp.stat = ALLNET_STAT.bad_shop.value
|
||||||
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
|
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
|
||||||
return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8")
|
return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n")
|
||||||
|
|
||||||
|
|
||||||
country = (
|
country = (
|
||||||
@@ -166,7 +212,7 @@ class AllnetServlet:
|
|||||||
country = AllnetCountryCode.JAPAN.value
|
country = AllnetCountryCode.JAPAN.value
|
||||||
|
|
||||||
resp.country = country
|
resp.country = country
|
||||||
resp.place_id = arcade["id"]
|
resp.place_id = f"{arcade['id']:04X}"
|
||||||
resp.allnet_id = machine["id"]
|
resp.allnet_id = machine["id"]
|
||||||
resp.name = arcade["name"] if arcade["name"] is not None else ""
|
resp.name = arcade["name"] if arcade["name"] is not None else ""
|
||||||
resp.nickname = arcade["nickname"] if arcade["nickname"] is not None else ""
|
resp.nickname = arcade["nickname"] if arcade["nickname"] is not None else ""
|
||||||
@@ -176,53 +222,57 @@ class AllnetServlet:
|
|||||||
else AllnetJapanRegionId.AICHI.value
|
else AllnetJapanRegionId.AICHI.value
|
||||||
)
|
)
|
||||||
resp.region_name0 = (
|
resp.region_name0 = (
|
||||||
arcade["country"]
|
|
||||||
if arcade["country"] is not None
|
|
||||||
else AllnetCountryCode.JAPAN.value
|
|
||||||
)
|
|
||||||
resp.region_name1 = (
|
|
||||||
arcade["state"]
|
arcade["state"]
|
||||||
if arcade["state"] is not None
|
if arcade["state"] is not None
|
||||||
else AllnetJapanRegionId.AICHI.name
|
else AllnetJapanRegionId.AICHI.name
|
||||||
)
|
)
|
||||||
|
resp.region_name1 = (
|
||||||
|
arcade["country"]
|
||||||
|
if arcade["country"] is not None
|
||||||
|
else AllnetCountryCode.JAPAN.value
|
||||||
|
)
|
||||||
resp.region_name2 = arcade["city"] if arcade["city"] is not None else ""
|
resp.region_name2 = arcade["city"] if arcade["city"] is not None else ""
|
||||||
resp.client_timezone = (
|
resp.client_timezone = ( # lmao
|
||||||
arcade["timezone"] if arcade["timezone"] is not None else "+0900"
|
arcade["timezone"] if arcade["timezone"] is not None else "+0900" if req.format_ver == 3 else "+09:00"
|
||||||
)
|
)
|
||||||
|
|
||||||
if req.game_id not in self.uri_registry:
|
if req.game_id not in TitleServlet.title_registry:
|
||||||
if not self.config.server.is_develop:
|
if not self.config.server.is_develop:
|
||||||
msg = f"Unrecognised game {req.game_id} attempted allnet auth from {request_ip}."
|
msg = f"Unrecognised game {req.game_id} attempted allnet auth from {request_ip}."
|
||||||
self.data.base.log_event(
|
await self.data.base.log_event(
|
||||||
"allnet", "ALLNET_AUTH_UNKNOWN_GAME", logging.WARN, msg
|
"allnet", "ALLNET_AUTH_UNKNOWN_GAME", logging.WARN, msg
|
||||||
)
|
)
|
||||||
self.logger.warning(msg)
|
self.logger.warning(msg)
|
||||||
|
|
||||||
resp.stat = ALLNET_STAT.bad_game.value
|
resp.stat = ALLNET_STAT.bad_game.value
|
||||||
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
|
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
|
||||||
return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8")
|
return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
f"Allowed unknown game {req.game_id} v{req.ver} to authenticate from {request_ip} due to 'is_develop' being enabled. S/N: {req.serial}"
|
f"Allowed unknown game {req.game_id} v{req.ver} to authenticate from {request_ip} due to 'is_develop' being enabled. S/N: {req.serial}"
|
||||||
)
|
)
|
||||||
resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/{req.game_id}/{req.ver.replace('.', '')}/"
|
resp.uri = f"http://{self.config.server.hostname}:{self.config.server.port}/{req.game_id}/{req.ver.replace('.', '')}/"
|
||||||
resp.host = f"{self.config.title.hostname}:{self.config.title.port}"
|
resp.host = f"{self.config.server.hostname}:{self.config.server.port}"
|
||||||
|
|
||||||
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
|
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
|
||||||
resp_str = urllib.parse.unquote(urllib.parse.urlencode(resp_dict))
|
resp_str = urllib.parse.unquote(urllib.parse.urlencode(resp_dict))
|
||||||
|
|
||||||
self.logger.debug(f"Allnet response: {resp_str}")
|
self.logger.debug(f"Allnet response: {resp_str}")
|
||||||
return (resp_str + "\n").encode("utf-8")
|
return PlainTextResponse(resp_str + "\n")
|
||||||
|
|
||||||
resp.uri, resp.host = self.uri_registry[req.game_id]
|
|
||||||
|
|
||||||
int_ver = req.ver.replace(".", "")
|
int_ver = req.ver.replace(".", "")
|
||||||
resp.uri = resp.uri.replace("$v", int_ver)
|
try:
|
||||||
resp.host = resp.host.replace("$v", int_ver)
|
resp.uri, resp.host = TitleServlet.title_registry[req.game_id].get_allnet_info(req.game_id, int(int_ver), req.serial)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error running get_allnet_info for {req.game_id} - {e}")
|
||||||
|
resp.stat = ALLNET_STAT.bad_game.value
|
||||||
|
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
|
||||||
|
return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n")
|
||||||
|
|
||||||
msg = f"{req.serial} authenticated from {request_ip}: {req.game_id} v{req.ver}"
|
msg = f"{req.serial} authenticated from {request_ip}: {req.game_id} v{req.ver}"
|
||||||
self.data.base.log_event("allnet", "ALLNET_AUTH_SUCCESS", logging.INFO, msg)
|
await self.data.base.log_event("allnet", "ALLNET_AUTH_SUCCESS", logging.INFO, msg)
|
||||||
self.logger.info(msg)
|
self.logger.info(msg)
|
||||||
|
|
||||||
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
|
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
|
||||||
@@ -234,18 +284,19 @@ class AllnetServlet:
|
|||||||
request.responseHeaders.addRawHeader('Pragma', 'DFI')
|
request.responseHeaders.addRawHeader('Pragma', 'DFI')
|
||||||
return self.to_dfi(resp_str)"""
|
return self.to_dfi(resp_str)"""
|
||||||
|
|
||||||
return resp_str.encode("utf-8")
|
return PlainTextResponse(resp_str)
|
||||||
|
|
||||||
def handle_dlorder(self, request: Request, _: Dict):
|
async def handle_dlorder(self, request: Request):
|
||||||
request_ip = Utils.get_ip_addr(request)
|
request_ip = Utils.get_ip_addr(request)
|
||||||
pragma_header = request.getHeader('Pragma')
|
pragma_header = request.headers.get('Pragma', "")
|
||||||
is_dfi = pragma_header is not None and pragma_header == "DFI"
|
is_dfi = pragma_header is not None and pragma_header == "DFI"
|
||||||
|
data = await request.body()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if is_dfi:
|
if is_dfi:
|
||||||
req_urlencode = self.from_dfi(request.content.getvalue())
|
req_urlencode = self.from_dfi(data)
|
||||||
else:
|
else:
|
||||||
req_urlencode = request.content.getvalue().decode()
|
req_urlencode = data.decode()
|
||||||
|
|
||||||
req_dict = self.allnet_req_to_dict(req_urlencode)
|
req_dict = self.allnet_req_to_dict(req_urlencode)
|
||||||
if req_dict is None:
|
if req_dict is None:
|
||||||
@@ -262,7 +313,7 @@ class AllnetServlet:
|
|||||||
except AllnetRequestException as e:
|
except AllnetRequestException as e:
|
||||||
if e.message != "":
|
if e.message != "":
|
||||||
self.logger.error(e)
|
self.logger.error(e)
|
||||||
return b""
|
return PlainTextResponse()
|
||||||
|
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
f"DownloadOrder from {request_ip} -> {req.game_id} v{req.ver} serial {req.serial}"
|
f"DownloadOrder from {request_ip} -> {req.game_id} v{req.ver} serial {req.serial}"
|
||||||
@@ -273,54 +324,54 @@ class AllnetServlet:
|
|||||||
not self.config.allnet.allow_online_updates
|
not self.config.allnet.allow_online_updates
|
||||||
or not self.config.allnet.update_cfg_folder
|
or not self.config.allnet.update_cfg_folder
|
||||||
):
|
):
|
||||||
return urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n"
|
return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n")
|
||||||
|
|
||||||
else: # TODO: Keychip check
|
else: # TODO: Keychip check
|
||||||
if path.exists(
|
if path.exists(
|
||||||
f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver.replace('.', '')}-app.ini"
|
f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver.replace('.', '')}-app.ini"
|
||||||
):
|
):
|
||||||
resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-app.ini"
|
resp.uri = f"http://{self.config.server.hostname}:{self.config.server.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-app.ini"
|
||||||
|
|
||||||
if path.exists(
|
if path.exists(
|
||||||
f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver.replace('.', '')}-opt.ini"
|
f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver.replace('.', '')}-opt.ini"
|
||||||
):
|
):
|
||||||
resp.uri += f"|http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-opt.ini"
|
resp.uri += f"|http://{self.config.server.hostname}:{self.config.server.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-opt.ini"
|
||||||
|
|
||||||
self.logger.debug(f"Sending download uri {resp.uri}")
|
self.logger.debug(f"Sending download uri {resp.uri}")
|
||||||
self.data.base.log_event("allnet", "DLORDER_REQ_SUCCESS", logging.INFO, f"{Utils.get_ip_addr(request)} requested DL Order for {req.serial} {req.game_id} v{req.ver}")
|
await self.data.base.log_event("allnet", "DLORDER_REQ_SUCCESS", logging.INFO, f"{Utils.get_ip_addr(request)} requested DL Order for {req.serial} {req.game_id} v{req.ver}")
|
||||||
|
|
||||||
res_str = urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n"
|
res_str = urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n"
|
||||||
"""if is_dfi:
|
"""if is_dfi:
|
||||||
request.responseHeaders.addRawHeader('Pragma', 'DFI')
|
request.responseHeaders.addRawHeader('Pragma', 'DFI')
|
||||||
return self.to_dfi(res_str)"""
|
return self.to_dfi(res_str)"""
|
||||||
|
|
||||||
return res_str
|
return PlainTextResponse(res_str)
|
||||||
|
|
||||||
|
async def handle_dlorder_ini(self, request: Request) -> bytes:
|
||||||
|
req_file = request.path_params.get("file", "").replace("%0A", "").replace("\n", "")
|
||||||
|
|
||||||
def handle_dlorder_ini(self, request: Request, match: Dict) -> bytes:
|
if not req_file:
|
||||||
if "file" not in match:
|
return PlainTextResponse(status_code=404)
|
||||||
return b""
|
|
||||||
|
|
||||||
req_file = match["file"].replace("%0A", "")
|
|
||||||
|
|
||||||
if path.exists(f"{self.config.allnet.update_cfg_folder}/{req_file}"):
|
if path.exists(f"{self.config.allnet.update_cfg_folder}/{req_file}"):
|
||||||
self.logger.info(f"Request for DL INI file {req_file} from {Utils.get_ip_addr(request)} successful")
|
self.logger.info(f"Request for DL INI file {req_file} from {Utils.get_ip_addr(request)} successful")
|
||||||
self.data.base.log_event("allnet", "DLORDER_INI_SENT", logging.INFO, f"{Utils.get_ip_addr(request)} successfully recieved {req_file}")
|
await self.data.base.log_event("allnet", "DLORDER_INI_SENT", logging.INFO, f"{Utils.get_ip_addr(request)} successfully recieved {req_file}")
|
||||||
|
|
||||||
return open(
|
return PlainTextResponse(open(
|
||||||
f"{self.config.allnet.update_cfg_folder}/{req_file}", "rb"
|
f"{self.config.allnet.update_cfg_folder}/{req_file}", "r", encoding="utf-8"
|
||||||
).read()
|
).read())
|
||||||
|
|
||||||
self.logger.info(f"DL INI File {req_file} not found")
|
self.logger.info(f"DL INI File {req_file} not found")
|
||||||
return b""
|
return PlainTextResponse()
|
||||||
|
|
||||||
def handle_dlorder_report(self, request: Request, match: Dict) -> bytes:
|
async def handle_dlorder_report(self, request: Request) -> bytes:
|
||||||
req_raw = request.content.getvalue()
|
req_raw = await request.body()
|
||||||
|
client_ip = Utils.get_ip_addr(request)
|
||||||
try:
|
try:
|
||||||
req_dict: Dict = json.loads(req_raw)
|
req_dict: Dict = json.loads(req_raw)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning(f"Failed to parse DL Report: {e}")
|
self.logger.warning(f"Failed to parse DL Report: {e}")
|
||||||
return "NG"
|
return PlainTextResponse("NG")
|
||||||
|
|
||||||
dl_data_type = DLIMG_TYPE.app
|
dl_data_type = DLIMG_TYPE.app
|
||||||
dl_data = req_dict.get("appimage", {})
|
dl_data = req_dict.get("appimage", {})
|
||||||
@@ -331,18 +382,24 @@ class AllnetServlet:
|
|||||||
|
|
||||||
if dl_data is None or not dl_data:
|
if dl_data is None or not dl_data:
|
||||||
self.logger.warning(f"Failed to parse DL Report: Invalid format - contains neither appimage nor optimage")
|
self.logger.warning(f"Failed to parse DL Report: Invalid format - contains neither appimage nor optimage")
|
||||||
return "NG"
|
return PlainTextResponse("NG")
|
||||||
|
|
||||||
dl_report_data = DLReport(dl_data, dl_data_type)
|
rep = DLReport(dl_data, dl_data_type)
|
||||||
|
|
||||||
if not dl_report_data.validate():
|
if not rep.validate():
|
||||||
self.logger.warning(f"Failed to parse DL Report: Invalid format - {dl_report_data.err}")
|
self.logger.warning(f"Failed to parse DL Report: Invalid format - {rep.err}")
|
||||||
return "NG"
|
return PlainTextResponse("NG")
|
||||||
|
|
||||||
return "OK"
|
msg = f"{rep.serial} @ {client_ip} reported {rep.rep_type.name} download state {rep.rf_state.name} for {rep.gd} v{rep.dav}:"\
|
||||||
|
f" {rep.tdsc}/{rep.tsc} segments downloaded for working files {rep.wfl} with {rep.dfl if rep.dfl else 'none'} complete."
|
||||||
|
|
||||||
def handle_loaderstaterecorder(self, request: Request, match: Dict) -> bytes:
|
await self.data.base.log_event("allnet", "DL_REPORT", logging.INFO, msg, dl_data)
|
||||||
req_data = request.content.getvalue()
|
self.logger.info(msg)
|
||||||
|
|
||||||
|
return PlainTextResponse("OK")
|
||||||
|
|
||||||
|
async def handle_loaderstaterecorder(self, request: Request) -> bytes:
|
||||||
|
req_data = await request.body()
|
||||||
sections = req_data.decode("utf-8").split("\r\n")
|
sections = req_data.decode("utf-8").split("\r\n")
|
||||||
|
|
||||||
req_dict = dict(urllib.parse.parse_qsl(sections[0]))
|
req_dict = dict(urllib.parse.parse_qsl(sections[0]))
|
||||||
@@ -354,129 +411,17 @@ class AllnetServlet:
|
|||||||
ip = Utils.get_ip_addr(request)
|
ip = Utils.get_ip_addr(request)
|
||||||
|
|
||||||
if serial is None or num_files_dld is None or num_files_to_dl is None or dl_state is None:
|
if serial is None or num_files_dld is None or num_files_to_dl is None or dl_state is None:
|
||||||
return "NG".encode()
|
return PlainTextResponse("NG")
|
||||||
|
|
||||||
self.logger.info(f"LoaderStateRecorder Request from {ip} {serial}: {num_files_dld}/{num_files_to_dl} Files download (State: {dl_state})")
|
self.logger.info(f"LoaderStateRecorder Request from {ip} {serial}: {num_files_dld}/{num_files_to_dl} Files download (State: {dl_state})")
|
||||||
return "OK".encode()
|
return PlainTextResponse("OK")
|
||||||
|
|
||||||
def handle_alive(self, request: Request, match: Dict) -> bytes:
|
async def handle_alive(self, request: Request) -> bytes:
|
||||||
return "OK".encode()
|
return PlainTextResponse("OK")
|
||||||
|
|
||||||
def handle_billing_request(self, request: Request, _: Dict):
|
async def handle_naomitest(self, request: Request) -> bytes:
|
||||||
req_raw = request.content.getvalue()
|
|
||||||
|
|
||||||
if request.getHeader('Content-Type') == "application/octet-stream":
|
|
||||||
req_unzip = zlib.decompressobj(-zlib.MAX_WBITS).decompress(req_raw)
|
|
||||||
else:
|
|
||||||
req_unzip = req_raw
|
|
||||||
|
|
||||||
req_dict = self.billing_req_to_dict(req_unzip)
|
|
||||||
request_ip = Utils.get_ip_addr(request)
|
|
||||||
|
|
||||||
if req_dict is None:
|
|
||||||
self.logger.error(f"Failed to parse request {request.content.getvalue()}")
|
|
||||||
return b""
|
|
||||||
|
|
||||||
self.logger.debug(f"request {req_dict}")
|
|
||||||
|
|
||||||
rsa = RSA.import_key(open(self.config.billing.signing_key, "rb").read())
|
|
||||||
signer = PKCS1_v1_5.new(rsa)
|
|
||||||
digest = SHA.new()
|
|
||||||
traces: List[TraceData] = []
|
|
||||||
|
|
||||||
try:
|
|
||||||
for x in range(len(req_dict)):
|
|
||||||
if not req_dict[x]:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if x == 0:
|
|
||||||
req = BillingInfo(req_dict[x])
|
|
||||||
continue
|
|
||||||
|
|
||||||
tmp = TraceData(req_dict[x])
|
|
||||||
if tmp.trace_type == TraceDataType.CHARGE:
|
|
||||||
tmp = TraceDataCharge(req_dict[x])
|
|
||||||
elif tmp.trace_type == TraceDataType.EVENT:
|
|
||||||
tmp = TraceDataEvent(req_dict[x])
|
|
||||||
elif tmp.trace_type == TraceDataType.CREDIT:
|
|
||||||
tmp = TraceDataCredit(req_dict[x])
|
|
||||||
|
|
||||||
traces.append(tmp)
|
|
||||||
|
|
||||||
kc_serial_bytes = req.keychipid.encode()
|
|
||||||
|
|
||||||
except KeyError as e:
|
|
||||||
self.logger.error(f"Billing request failed to parse: {e}")
|
|
||||||
return f"result=5&linelimit=&message=field is missing or formatting is incorrect\r\n".encode()
|
|
||||||
|
|
||||||
machine = self.data.arcade.get_machine(req.keychipid)
|
|
||||||
if machine is None and not self.config.server.allow_unregistered_serials:
|
|
||||||
msg = f"Unrecognised serial {req.keychipid} attempted billing checkin from {request_ip} for {req.gameid} v{req.gamever}."
|
|
||||||
self.data.base.log_event(
|
|
||||||
"allnet", "BILLING_CHECKIN_NG_SERIAL", logging.WARN, msg
|
|
||||||
)
|
|
||||||
self.logger.warning(msg)
|
|
||||||
|
|
||||||
return f"result=1&requestno={req.requestno}&message=Keychip Serial bad\r\n".encode()
|
|
||||||
|
|
||||||
msg = (
|
|
||||||
f"Billing checkin from {request_ip}: game {req.gameid} ver {req.gamever} keychip {req.keychipid} playcount "
|
|
||||||
f"{req.playcnt} billing_type {req.billingtype.name} nearfull {req.nearfull} playlimit {req.playlimit}"
|
|
||||||
)
|
|
||||||
self.logger.info(msg)
|
|
||||||
self.data.base.log_event("billing", "BILLING_CHECKIN_OK", logging.INFO, msg)
|
|
||||||
if req.traceleft > 0:
|
|
||||||
self.logger.warn(f"{req.traceleft} unsent tracelogs")
|
|
||||||
kc_playlimit = req.playlimit
|
|
||||||
kc_nearfull = req.nearfull
|
|
||||||
|
|
||||||
while req.playcnt > req.playlimit:
|
|
||||||
kc_playlimit += 1024
|
|
||||||
kc_nearfull += 1024
|
|
||||||
|
|
||||||
playlimit = kc_playlimit
|
|
||||||
nearfull = kc_nearfull + (req.billingtype.value * 0x00010000)
|
|
||||||
|
|
||||||
digest.update(playlimit.to_bytes(4, "little") + kc_serial_bytes)
|
|
||||||
playlimit_sig = signer.sign(digest).hex()
|
|
||||||
|
|
||||||
digest = SHA.new()
|
|
||||||
digest.update(nearfull.to_bytes(4, "little") + kc_serial_bytes)
|
|
||||||
nearfull_sig = signer.sign(digest).hex()
|
|
||||||
|
|
||||||
# TODO: playhistory
|
|
||||||
|
|
||||||
#resp = BillingResponse(playlimit, playlimit_sig, nearfull, nearfull_sig)
|
|
||||||
resp = BillingResponse(playlimit, playlimit_sig, nearfull, nearfull_sig, req.requestno, req.protocolver)
|
|
||||||
|
|
||||||
resp_str = urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\r\n"
|
|
||||||
|
|
||||||
self.logger.debug(f"response {vars(resp)}")
|
|
||||||
if req.traceleft > 0:
|
|
||||||
self.logger.info(f"Requesting 20 more of {req.traceleft} unsent tracelogs")
|
|
||||||
return f"result=6&waittime=0&linelimit=20\r\n".encode()
|
|
||||||
|
|
||||||
return resp_str.encode("utf-8")
|
|
||||||
|
|
||||||
def handle_naomitest(self, request: Request, _: Dict) -> bytes:
|
|
||||||
self.logger.info(f"Ping from {Utils.get_ip_addr(request)}")
|
self.logger.info(f"Ping from {Utils.get_ip_addr(request)}")
|
||||||
return b"naomi ok"
|
return PlainTextResponse("naomi ok")
|
||||||
|
|
||||||
def billing_req_to_dict(self, data: bytes):
|
|
||||||
"""
|
|
||||||
Parses an billing request string into a python dictionary
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
sections = data.decode("ascii").split("\r\n")
|
|
||||||
|
|
||||||
ret = []
|
|
||||||
for x in sections:
|
|
||||||
ret.append(dict(urllib.parse.parse_qsl(x)))
|
|
||||||
return ret
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error(f"billing_req_to_dict: {e} while parsing {data}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def allnet_req_to_dict(self, data: str) -> Optional[List[Dict[str, Any]]]:
|
def allnet_req_to_dict(self, data: str) -> Optional[List[Dict[str, Any]]]:
|
||||||
"""
|
"""
|
||||||
@@ -504,6 +449,150 @@ class AllnetServlet:
|
|||||||
zipped = zlib.compress(unzipped)
|
zipped = zlib.compress(unzipped)
|
||||||
return base64.b64encode(zipped)
|
return base64.b64encode(zipped)
|
||||||
|
|
||||||
|
class BillingServlet:
|
||||||
|
def __init__(self, core_cfg: CoreConfig, cfg_folder: str) -> None:
|
||||||
|
self.config = core_cfg
|
||||||
|
self.config_folder = cfg_folder
|
||||||
|
self.data = Data(core_cfg)
|
||||||
|
|
||||||
|
self.logger = logging.getLogger("billing")
|
||||||
|
if not hasattr(self.logger, "initialized"):
|
||||||
|
log_fmt_str = "[%(asctime)s] Billing | %(levelname)s | %(message)s"
|
||||||
|
log_fmt = logging.Formatter(log_fmt_str)
|
||||||
|
|
||||||
|
fileHandler = TimedRotatingFileHandler(
|
||||||
|
"{0}/{1}.log".format(self.config.server.log_dir, "billing"),
|
||||||
|
when="d",
|
||||||
|
backupCount=10,
|
||||||
|
)
|
||||||
|
fileHandler.setFormatter(log_fmt)
|
||||||
|
|
||||||
|
consoleHandler = logging.StreamHandler()
|
||||||
|
consoleHandler.setFormatter(log_fmt)
|
||||||
|
|
||||||
|
self.logger.addHandler(fileHandler)
|
||||||
|
self.logger.addHandler(consoleHandler)
|
||||||
|
|
||||||
|
self.logger.setLevel(core_cfg.allnet.loglevel)
|
||||||
|
coloredlogs.install(
|
||||||
|
level=core_cfg.billing.loglevel, logger=self.logger, fmt=log_fmt_str
|
||||||
|
)
|
||||||
|
self.logger.initialized = True
|
||||||
|
|
||||||
|
def startup(self) -> None:
|
||||||
|
self.logger.info(f"Ready on port {self.config.billing.port if self.config.billing.standalone else self.config.server.port}")
|
||||||
|
|
||||||
|
def billing_req_to_dict(self, data: bytes):
|
||||||
|
"""
|
||||||
|
Parses an billing request string into a python dictionary
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
sections = data.decode("ascii").split("\r\n")
|
||||||
|
|
||||||
|
ret = []
|
||||||
|
for x in sections:
|
||||||
|
ret.append(dict(urllib.parse.parse_qsl(x)))
|
||||||
|
return ret
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"billing_req_to_dict: {e} while parsing {data}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def handle_billing_request(self, request: Request):
|
||||||
|
req_raw = await request.body()
|
||||||
|
|
||||||
|
if request.headers.get('Content-Type', '') == "application/octet-stream":
|
||||||
|
req_unzip = zlib.decompressobj(-zlib.MAX_WBITS).decompress(req_raw)
|
||||||
|
else:
|
||||||
|
req_unzip = req_raw
|
||||||
|
|
||||||
|
req_dict = self.billing_req_to_dict(req_unzip)
|
||||||
|
request_ip = Utils.get_ip_addr(request)
|
||||||
|
|
||||||
|
if req_dict is None:
|
||||||
|
self.logger.error(f"Failed to parse request {req_raw}")
|
||||||
|
return PlainTextResponse()
|
||||||
|
|
||||||
|
self.logger.debug(f"request {req_dict}")
|
||||||
|
|
||||||
|
rsa = RSA.import_key(open(self.config.billing.signing_key, "rb").read())
|
||||||
|
signer = PKCS1_v1_5.new(rsa)
|
||||||
|
digest = SHA.new()
|
||||||
|
traces: List[TraceData] = []
|
||||||
|
try:
|
||||||
|
req = BillingInfo(req_dict[0])
|
||||||
|
except KeyError as e:
|
||||||
|
self.logger.error(f"Billing request failed to parse: {e}")
|
||||||
|
return PlainTextResponse("result=5&linelimit=&message=field is missing or formatting is incorrect\r\n")
|
||||||
|
|
||||||
|
for x in range(1, len(req_dict)):
|
||||||
|
if not req_dict[x]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
tmp = TraceData(req_dict[x])
|
||||||
|
if tmp.trace_type == TraceDataType.CHARGE:
|
||||||
|
tmp = TraceDataCharge(req_dict[x])
|
||||||
|
elif tmp.trace_type == TraceDataType.EVENT:
|
||||||
|
tmp = TraceDataEvent(req_dict[x])
|
||||||
|
elif tmp.trace_type == TraceDataType.CREDIT:
|
||||||
|
tmp = TraceDataCredit(req_dict[x])
|
||||||
|
|
||||||
|
traces.append(tmp)
|
||||||
|
|
||||||
|
except KeyError as e:
|
||||||
|
self.logger.warn(f"Tracelog failed to parse: {e}")
|
||||||
|
|
||||||
|
kc_serial_bytes = req.keychipid.encode()
|
||||||
|
|
||||||
|
|
||||||
|
machine = await self.data.arcade.get_machine(req.keychipid)
|
||||||
|
if machine is None and not self.config.server.allow_unregistered_serials:
|
||||||
|
msg = f"Unrecognised serial {req.keychipid} attempted billing checkin from {request_ip} for {req.gameid} v{req.gamever}."
|
||||||
|
await self.data.base.log_event(
|
||||||
|
"allnet", "BILLING_CHECKIN_NG_SERIAL", logging.WARN, msg
|
||||||
|
)
|
||||||
|
self.logger.warning(msg)
|
||||||
|
|
||||||
|
return PlainTextResponse(f"result=1&requestno={req.requestno}&message=Keychip Serial bad\r\n")
|
||||||
|
|
||||||
|
msg = (
|
||||||
|
f"Billing checkin from {request_ip}: game {req.gameid} ver {req.gamever} keychip {req.keychipid} playcount "
|
||||||
|
f"{req.playcnt} billing_type {req.billingtype.name} nearfull {req.nearfull} playlimit {req.playlimit}"
|
||||||
|
)
|
||||||
|
self.logger.info(msg)
|
||||||
|
await self.data.base.log_event("billing", "BILLING_CHECKIN_OK", logging.INFO, msg)
|
||||||
|
if req.traceleft > 0:
|
||||||
|
self.logger.warn(f"{req.traceleft} unsent tracelogs")
|
||||||
|
kc_playlimit = req.playlimit
|
||||||
|
kc_nearfull = req.nearfull
|
||||||
|
|
||||||
|
while req.playcnt > req.playlimit:
|
||||||
|
kc_playlimit += 1024
|
||||||
|
kc_nearfull += 1024
|
||||||
|
|
||||||
|
playlimit = kc_playlimit
|
||||||
|
nearfull = kc_nearfull + (req.billingtype.value * 0x00010000)
|
||||||
|
|
||||||
|
digest.update(playlimit.to_bytes(4, "little") + kc_serial_bytes)
|
||||||
|
playlimit_sig = signer.sign(digest).hex()
|
||||||
|
|
||||||
|
digest = SHA.new()
|
||||||
|
digest.update(nearfull.to_bytes(4, "little") + kc_serial_bytes)
|
||||||
|
nearfull_sig = signer.sign(digest).hex()
|
||||||
|
|
||||||
|
# TODO: playhistory
|
||||||
|
|
||||||
|
resp = BillingResponse(playlimit, playlimit_sig, nearfull, nearfull_sig, req.requestno, req.protocolver)
|
||||||
|
|
||||||
|
resp_str = urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\r\n"
|
||||||
|
|
||||||
|
self.logger.debug(f"response {vars(resp)}")
|
||||||
|
if req.traceleft > 0:
|
||||||
|
self.logger.info(f"Requesting 20 more of {req.traceleft} unsent tracelogs")
|
||||||
|
return PlainTextResponse("result=6&waittime=0&linelimit=20\r\n")
|
||||||
|
|
||||||
|
return PlainTextResponse(resp_str)
|
||||||
|
|
||||||
class AllnetPowerOnRequest:
|
class AllnetPowerOnRequest:
|
||||||
def __init__(self, req: Dict) -> None:
|
def __init__(self, req: Dict) -> None:
|
||||||
@@ -525,7 +614,7 @@ class AllnetPowerOnResponse:
|
|||||||
self.stat = 1
|
self.stat = 1
|
||||||
self.uri = ""
|
self.uri = ""
|
||||||
self.host = ""
|
self.host = ""
|
||||||
self.place_id = "123"
|
self.place_id = "0123"
|
||||||
self.name = "ARTEMiS"
|
self.name = "ARTEMiS"
|
||||||
self.nickname = "ARTEMiS"
|
self.nickname = "ARTEMiS"
|
||||||
self.region0 = "1"
|
self.region0 = "1"
|
||||||
@@ -563,7 +652,6 @@ class AllnetPowerOnResponse3(AllnetPowerOnResponse):
|
|||||||
self.minute = None
|
self.minute = None
|
||||||
self.second = None
|
self.second = None
|
||||||
|
|
||||||
|
|
||||||
class AllnetPowerOnResponse2(AllnetPowerOnResponse):
|
class AllnetPowerOnResponse2(AllnetPowerOnResponse):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -573,7 +661,6 @@ class AllnetPowerOnResponse2(AllnetPowerOnResponse):
|
|||||||
self.timezone = "+09:00"
|
self.timezone = "+09:00"
|
||||||
self.res_class = "PowerOnResponseV2"
|
self.res_class = "PowerOnResponseV2"
|
||||||
|
|
||||||
|
|
||||||
class AllnetDownloadOrderRequest:
|
class AllnetDownloadOrderRequest:
|
||||||
def __init__(self, req: Dict) -> None:
|
def __init__(self, req: Dict) -> None:
|
||||||
self.game_id = req.get("game_id", "")
|
self.game_id = req.get("game_id", "")
|
||||||
@@ -581,7 +668,6 @@ class AllnetDownloadOrderRequest:
|
|||||||
self.serial = req.get("serial", "")
|
self.serial = req.get("serial", "")
|
||||||
self.encode = req.get("encode", "")
|
self.encode = req.get("encode", "")
|
||||||
|
|
||||||
|
|
||||||
class AllnetDownloadOrderResponse:
|
class AllnetDownloadOrderResponse:
|
||||||
def __init__(self, stat: int = 1, serial: str = "", uri: str = "") -> None:
|
def __init__(self, stat: int = 1, serial: str = "", uri: str = "") -> None:
|
||||||
self.stat = stat
|
self.stat = stat
|
||||||
@@ -644,9 +730,17 @@ class TraceData:
|
|||||||
self.seq_number = int(data.get("sn", None))
|
self.seq_number = int(data.get("sn", None))
|
||||||
self.trace_type = TraceDataType(int(data.get("tt", None)))
|
self.trace_type = TraceDataType(int(data.get("tt", None)))
|
||||||
self.date_sync_flg = bool(data.get("ds", None))
|
self.date_sync_flg = bool(data.get("ds", None))
|
||||||
self.date = datetime.strptime(data.get("dt", None), BILLING_DT_FORMAT)
|
|
||||||
|
dt = data.get("dt", None)
|
||||||
|
if dt is None:
|
||||||
|
raise KeyError("dt not present")
|
||||||
|
if dt == "20000000000000": # Not sure what causes it to send like this...
|
||||||
|
self.date = datetime(2000, 1, 1, 0, 0, 0, 0)
|
||||||
|
else:
|
||||||
|
self.date = datetime.strptime(data.get("dt", None), BILLING_DT_FORMAT)
|
||||||
|
|
||||||
self.keychip = str(data.get("kn", None))
|
self.keychip = str(data.get("kn", None))
|
||||||
self.lib_ver = float(data.get("alib", None))
|
self.lib_ver = float(data.get("alib", 0))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise KeyError(e)
|
raise KeyError(e)
|
||||||
|
|
||||||
@@ -654,8 +748,8 @@ class TraceDataCharge(TraceData):
|
|||||||
def __init__(self, data: Dict) -> None:
|
def __init__(self, data: Dict) -> None:
|
||||||
super().__init__(data)
|
super().__init__(data)
|
||||||
try:
|
try:
|
||||||
self.game_id = str(data.get("gi", None))
|
self.game_id = str(data.get("gi", None)) # these seem optional...?
|
||||||
self.game_version = float(data.get("gv", None))
|
self.game_version = float(data.get("gv", 0))
|
||||||
self.board_serial = str(data.get("bn", None))
|
self.board_serial = str(data.get("bn", None))
|
||||||
self.shop_ip = str(data.get("ti", None))
|
self.shop_ip = str(data.get("ti", None))
|
||||||
self.play_count = int(data.get("pc", None))
|
self.play_count = int(data.get("pc", None))
|
||||||
@@ -723,7 +817,6 @@ class BillingResponse:
|
|||||||
# playhistory -> YYYYMM/C:...
|
# playhistory -> YYYYMM/C:...
|
||||||
# YYYY -> 4 digit year, MM -> 2 digit month, C -> Playcount during that period
|
# YYYY -> 4 digit year, MM -> 2 digit month, C -> Playcount during that period
|
||||||
|
|
||||||
|
|
||||||
class AllnetRequestException(Exception):
|
class AllnetRequestException(Exception):
|
||||||
def __init__(self, message="") -> None:
|
def __init__(self, message="") -> None:
|
||||||
self.message = message
|
self.message = message
|
||||||
@@ -740,13 +833,13 @@ class DLReport:
|
|||||||
self.ot = data.get("ot")
|
self.ot = data.get("ot")
|
||||||
self.rt = data.get("rt")
|
self.rt = data.get("rt")
|
||||||
self.as_ = data.get("as")
|
self.as_ = data.get("as")
|
||||||
self.rf_state = data.get("rf_state")
|
self.rf_state = DLI_STATUS.from_int(data.get("rf_state"))
|
||||||
self.gd = data.get("gd")
|
self.gd = data.get("gd")
|
||||||
self.dav = data.get("dav")
|
self.dav = data.get("dav")
|
||||||
self.wdav = data.get("wdav") # app only
|
self.wdav = data.get("wdav") # app only
|
||||||
self.dov = data.get("dov")
|
self.dov = data.get("dov")
|
||||||
self.wdov = data.get("wdov") # app only
|
self.wdov = data.get("wdov") # app only
|
||||||
self.__type = report_type
|
self.rep_type = report_type
|
||||||
self.err = ""
|
self.err = ""
|
||||||
|
|
||||||
def validate(self) -> bool:
|
def validate(self) -> bool:
|
||||||
@@ -754,14 +847,6 @@ class DLReport:
|
|||||||
self.err = "serial not provided"
|
self.err = "serial not provided"
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if self.dfl is None:
|
|
||||||
self.err = "dfl not provided"
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self.wfl is None:
|
|
||||||
self.err = "wfl not provided"
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self.tsc is None:
|
if self.tsc is None:
|
||||||
self.err = "tsc not provided"
|
self.err = "tsc not provided"
|
||||||
return False
|
return False
|
||||||
@@ -770,18 +855,6 @@ class DLReport:
|
|||||||
self.err = "tdsc not provided"
|
self.err = "tdsc not provided"
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if self.at is None:
|
|
||||||
self.err = "at not provided"
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self.ot is None:
|
|
||||||
self.err = "ot not provided"
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self.rt is None:
|
|
||||||
self.err = "rt not provided"
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self.as_ is None:
|
if self.as_ is None:
|
||||||
self.err = "as not provided"
|
self.err = "as not provided"
|
||||||
return False
|
return False
|
||||||
@@ -802,12 +875,57 @@ class DLReport:
|
|||||||
self.err = "dov not provided"
|
self.err = "dov not provided"
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if (self.wdav is None or self.wdov is None) and self.__type == DLIMG_TYPE.app:
|
if (self.wdav is None or self.wdov is None) and self.rep_type == DLIMG_TYPE.app:
|
||||||
self.err = "wdav or wdov not provided in app image"
|
self.err = "wdav or wdov not provided in app image"
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if (self.wdav is not None or self.wdov is not None) and self.__type == DLIMG_TYPE.opt:
|
if (self.wdav is not None or self.wdov is not None) and self.rep_type == DLIMG_TYPE.opt:
|
||||||
self.err = "wdav or wdov provided in opt image"
|
self.err = "wdav or wdov provided in opt image"
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
cfg_dir = environ.get("DIANA_CFG_DIR", "config")
|
||||||
|
cfg: CoreConfig = CoreConfig()
|
||||||
|
if path.exists(f"{cfg_dir}/core.yaml"):
|
||||||
|
cfg.update(yaml.safe_load(open(f"{cfg_dir}/core.yaml")))
|
||||||
|
|
||||||
|
if not path.exists(cfg.server.log_dir):
|
||||||
|
mkdir(cfg.server.log_dir)
|
||||||
|
|
||||||
|
if not access(cfg.server.log_dir, W_OK):
|
||||||
|
print(
|
||||||
|
f"Log directory {cfg.server.log_dir} NOT writable, please check permissions"
|
||||||
|
)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
billing = BillingServlet(cfg, cfg_dir)
|
||||||
|
app_billing = Starlette(
|
||||||
|
cfg.server.is_develop,
|
||||||
|
[
|
||||||
|
Route("/request", billing.handle_billing_request, methods=["POST"]),
|
||||||
|
Route("/request/", billing.handle_billing_request, methods=["POST"]),
|
||||||
|
],
|
||||||
|
on_startup=[billing.startup]
|
||||||
|
)
|
||||||
|
|
||||||
|
allnet = AllnetServlet(cfg, cfg_dir)
|
||||||
|
route_lst = [
|
||||||
|
Route("/sys/servlet/PowerOn", allnet.handle_poweron, methods=["GET", "POST"]),
|
||||||
|
Route("/sys/servlet/DownloadOrder", allnet.handle_dlorder, methods=["GET", "POST"]),
|
||||||
|
Route("/sys/servlet/LoaderStateRecorder", allnet.handle_loaderstaterecorder, methods=["GET", "POST"]),
|
||||||
|
Route("/sys/servlet/Alive", allnet.handle_alive, methods=["GET", "POST"]),
|
||||||
|
Route("/naomitest.html", allnet.handle_naomitest),
|
||||||
|
]
|
||||||
|
|
||||||
|
if cfg.allnet.allow_online_updates:
|
||||||
|
route_lst += [
|
||||||
|
Route("/report-api/Report", allnet.handle_dlorder_report, methods=["POST"]),
|
||||||
|
Route("/dl/ini/{file:str}", allnet.handle_dlorder_ini),
|
||||||
|
]
|
||||||
|
|
||||||
|
app_allnet = Starlette(
|
||||||
|
cfg.server.is_develop,
|
||||||
|
route_lst,
|
||||||
|
on_startup=[allnet.startup]
|
||||||
|
)
|
||||||
|
|||||||
92
core/app.py
Normal file
92
core/app.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import yaml
|
||||||
|
import logging
|
||||||
|
import coloredlogs
|
||||||
|
from logging.handlers import TimedRotatingFileHandler
|
||||||
|
from starlette.routing import Route
|
||||||
|
from starlette.requests import Request
|
||||||
|
from starlette.applications import Starlette
|
||||||
|
from starlette.responses import PlainTextResponse
|
||||||
|
from os import environ, path, mkdir, W_OK, access
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from core import CoreConfig, TitleServlet, MuchaServlet, AllnetServlet, BillingServlet, AimedbServlette
|
||||||
|
from core.frontend import FrontendServlet
|
||||||
|
|
||||||
|
async def dummy_rt(request: Request):
|
||||||
|
return PlainTextResponse("Service OK")
|
||||||
|
|
||||||
|
cfg_dir = environ.get("ARTEMIS_CFG_DIR", "config")
|
||||||
|
cfg: CoreConfig = CoreConfig()
|
||||||
|
if path.exists(f"{cfg_dir}/core.yaml"):
|
||||||
|
cfg.update(yaml.safe_load(open(f"{cfg_dir}/core.yaml")))
|
||||||
|
|
||||||
|
if not path.exists(cfg.server.log_dir):
|
||||||
|
mkdir(cfg.server.log_dir)
|
||||||
|
|
||||||
|
if not access(cfg.server.log_dir, W_OK):
|
||||||
|
print(
|
||||||
|
f"Log directory {cfg.server.log_dir} NOT writable, please check permissions"
|
||||||
|
)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
logger = logging.getLogger("core")
|
||||||
|
log_fmt_str = "[%(asctime)s] Core | %(levelname)s | %(message)s"
|
||||||
|
log_fmt = logging.Formatter(log_fmt_str)
|
||||||
|
|
||||||
|
fileHandler = TimedRotatingFileHandler(
|
||||||
|
"{0}/{1}.log".format(cfg.server.log_dir, "core"), when="d", backupCount=10
|
||||||
|
)
|
||||||
|
fileHandler.setFormatter(log_fmt)
|
||||||
|
|
||||||
|
consoleHandler = logging.StreamHandler()
|
||||||
|
consoleHandler.setFormatter(log_fmt)
|
||||||
|
|
||||||
|
logger.addHandler(fileHandler)
|
||||||
|
logger.addHandler(consoleHandler)
|
||||||
|
|
||||||
|
log_lv = logging.DEBUG if cfg.server.is_develop else logging.INFO
|
||||||
|
logger.setLevel(log_lv)
|
||||||
|
coloredlogs.install(level=log_lv, logger=logger, fmt=log_fmt_str)
|
||||||
|
|
||||||
|
logger.info(f"Artemis starting in {'develop' if cfg.server.is_develop else 'production'} mode")
|
||||||
|
|
||||||
|
title = TitleServlet(cfg, cfg_dir) # This has to be loaded first to load plugins
|
||||||
|
mucha = MuchaServlet(cfg, cfg_dir)
|
||||||
|
|
||||||
|
route_lst: List[Route] = [
|
||||||
|
# Mucha
|
||||||
|
Route("/mucha_front/boardauth.do", mucha.handle_boardauth, methods=["POST"]),
|
||||||
|
Route("/mucha_front/updatacheck.do", mucha.handle_updatecheck, methods=["POST"]),
|
||||||
|
Route("/mucha_front/downloadstate.do", mucha.handle_dlstate, methods=["POST"]),
|
||||||
|
# General
|
||||||
|
Route("/", dummy_rt),
|
||||||
|
Route("/robots.txt", FrontendServlet.robots)
|
||||||
|
]
|
||||||
|
|
||||||
|
if not cfg.billing.standalone:
|
||||||
|
billing = BillingServlet(cfg, cfg_dir)
|
||||||
|
route_lst += [
|
||||||
|
Route("/request", billing.handle_billing_request, methods=["POST"]),
|
||||||
|
Route("/request/", billing.handle_billing_request, methods=["POST"]),
|
||||||
|
]
|
||||||
|
|
||||||
|
if not cfg.allnet.standalone:
|
||||||
|
allnet = AllnetServlet(cfg, cfg_dir)
|
||||||
|
route_lst += [
|
||||||
|
Route("/sys/servlet/PowerOn", allnet.handle_poweron, methods=["GET", "POST"]),
|
||||||
|
Route("/sys/servlet/DownloadOrder", allnet.handle_dlorder, methods=["GET", "POST"]),
|
||||||
|
Route("/sys/servlet/LoaderStateRecorder", allnet.handle_loaderstaterecorder, methods=["GET", "POST"]),
|
||||||
|
Route("/sys/servlet/Alive", allnet.handle_alive, methods=["GET", "POST"]),
|
||||||
|
Route("/naomitest.html", allnet.handle_naomitest),
|
||||||
|
]
|
||||||
|
|
||||||
|
if cfg.allnet.allow_online_updates:
|
||||||
|
route_lst += [
|
||||||
|
Route("/report-api/Report", allnet.handle_dlorder_report, methods=["POST"]),
|
||||||
|
Route("/dl/ini/{file:str}", allnet.handle_dlorder_ini),
|
||||||
|
]
|
||||||
|
|
||||||
|
for code, game in title.title_registry.items():
|
||||||
|
route_lst += game.get_routes()
|
||||||
|
|
||||||
|
app = Starlette(cfg.server.is_develop, route_lst)
|
||||||
169
core/config.py
169
core/config.py
@@ -1,17 +1,49 @@
|
|||||||
import logging, os
|
import logging, os
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
class ServerConfig:
|
class ServerConfig:
|
||||||
def __init__(self, parent_config: "CoreConfig") -> None:
|
def __init__(self, parent_config: "CoreConfig") -> None:
|
||||||
self.__config = parent_config
|
self.__config = parent_config
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def listen_address(self) -> str:
|
def listen_address(self) -> str:
|
||||||
|
"""
|
||||||
|
Address Artemis will bind to and listen on
|
||||||
|
"""
|
||||||
return CoreConfig.get_config_field(
|
return CoreConfig.get_config_field(
|
||||||
self.__config, "core", "server", "listen_address", default="127.0.0.1"
|
self.__config, "core", "server", "listen_address", default="127.0.0.1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hostname(self) -> str:
|
||||||
|
"""
|
||||||
|
Hostname sent to games
|
||||||
|
"""
|
||||||
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "server", "hostname", default="localhost"
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def port(self) -> int:
|
||||||
|
"""
|
||||||
|
Port the game will listen on
|
||||||
|
"""
|
||||||
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "server", "port", default=80
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ssl_key(self) -> str:
|
||||||
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "server", "ssl_key", default="cert/title.key"
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ssl_cert(self) -> str:
|
||||||
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "title", "ssl_cert", default="cert/title.pem"
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def allow_user_registration(self) -> bool:
|
def allow_user_registration(self) -> bool:
|
||||||
return CoreConfig.get_config_field(
|
return CoreConfig.get_config_field(
|
||||||
@@ -37,9 +69,29 @@ class ServerConfig:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def threading(self) -> bool:
|
def is_using_proxy(self) -> bool:
|
||||||
return CoreConfig.get_config_field(
|
return CoreConfig.get_config_field(
|
||||||
self.__config, "core", "server", "threading", default=False
|
self.__config, "core", "server", "is_using_proxy", default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def proxy_port(self) -> int:
|
||||||
|
"""
|
||||||
|
What port the proxy is listening on. This will be sent instead of 'port' if
|
||||||
|
is_using_proxy is True and this value is non-zero
|
||||||
|
"""
|
||||||
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "server", "proxy_port", default=0
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def proxy_port_ssl(self) -> int:
|
||||||
|
"""
|
||||||
|
What port the proxy is listening for secure connections on. This will be sent
|
||||||
|
instead of 'port' if is_using_proxy is True and this value is non-zero
|
||||||
|
"""
|
||||||
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "server", "proxy_port_ssl", default=0
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -60,7 +112,6 @@ class ServerConfig:
|
|||||||
self.__config, "core", "server", "strict_ip_checking", default=False
|
self.__config, "core", "server", "strict_ip_checking", default=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TitleConfig:
|
class TitleConfig:
|
||||||
def __init__(self, parent_config: "CoreConfig") -> None:
|
def __init__(self, parent_config: "CoreConfig") -> None:
|
||||||
self.__config = parent_config
|
self.__config = parent_config
|
||||||
@@ -74,18 +125,17 @@ class TitleConfig:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hostname(self) -> str:
|
def reboot_start_time(self) -> str:
|
||||||
return CoreConfig.get_config_field(
|
return CoreConfig.get_config_field(
|
||||||
self.__config, "core", "title", "hostname", default="localhost"
|
self.__config, "core", "title", "reboot_start_time", default=""
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def port(self) -> int:
|
def reboot_end_time(self) -> str:
|
||||||
return CoreConfig.get_config_field(
|
return CoreConfig.get_config_field(
|
||||||
self.__config, "core", "title", "port", default=8080
|
self.__config, "core", "title", "reboot_end_time", default=""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DatabaseConfig:
|
class DatabaseConfig:
|
||||||
def __init__(self, parent_config: "CoreConfig") -> None:
|
def __init__(self, parent_config: "CoreConfig") -> None:
|
||||||
self.__config = parent_config
|
self.__config = parent_config
|
||||||
@@ -123,7 +173,7 @@ class DatabaseConfig:
|
|||||||
@property
|
@property
|
||||||
def protocol(self) -> str:
|
def protocol(self) -> str:
|
||||||
return CoreConfig.get_config_field(
|
return CoreConfig.get_config_field(
|
||||||
self.__config, "core", "database", "type", default="mysql"
|
self.__config, "core", "database", "protocol", default="mysql"
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -141,13 +191,9 @@ class DatabaseConfig:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def user_table_autoincrement_start(self) -> int:
|
def enable_memcached(self) -> bool:
|
||||||
return CoreConfig.get_config_field(
|
return CoreConfig.get_config_field(
|
||||||
self.__config,
|
self.__config, "core", "database", "enable_memcached", default=True
|
||||||
"core",
|
|
||||||
"database",
|
|
||||||
"user_table_autoincrement_start",
|
|
||||||
default=10000,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -156,13 +202,12 @@ class DatabaseConfig:
|
|||||||
self.__config, "core", "database", "memcached_host", default="localhost"
|
self.__config, "core", "database", "memcached_host", default="localhost"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class FrontendConfig:
|
class FrontendConfig:
|
||||||
def __init__(self, parent_config: "CoreConfig") -> None:
|
def __init__(self, parent_config: "CoreConfig") -> None:
|
||||||
self.__config = parent_config
|
self.__config = parent_config
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def enable(self) -> int:
|
def enable(self) -> bool:
|
||||||
return CoreConfig.get_config_field(
|
return CoreConfig.get_config_field(
|
||||||
self.__config, "core", "frontend", "enable", default=False
|
self.__config, "core", "frontend", "enable", default=False
|
||||||
)
|
)
|
||||||
@@ -170,7 +215,7 @@ class FrontendConfig:
|
|||||||
@property
|
@property
|
||||||
def port(self) -> int:
|
def port(self) -> int:
|
||||||
return CoreConfig.get_config_field(
|
return CoreConfig.get_config_field(
|
||||||
self.__config, "core", "frontend", "port", default=8090
|
self.__config, "core", "frontend", "port", default=8080
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -181,17 +226,20 @@ class FrontendConfig:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def secret(self) -> str:
|
||||||
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "frontend", "secret", default=""
|
||||||
|
)
|
||||||
|
|
||||||
class AllnetConfig:
|
class AllnetConfig:
|
||||||
def __init__(self, parent_config: "CoreConfig") -> None:
|
def __init__(self, parent_config: "CoreConfig") -> None:
|
||||||
self.__config = parent_config
|
self.__config = parent_config
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def loglevel(self) -> int:
|
def standalone(self) -> bool:
|
||||||
return CoreConfig.str_to_loglevel(
|
return CoreConfig.get_config_field(
|
||||||
CoreConfig.get_config_field(
|
self.__config, "core", "allnet", "standalone", default=False
|
||||||
self.__config, "core", "allnet", "loglevel", default="info"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -201,9 +249,11 @@ class AllnetConfig:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ip_check(self) -> bool:
|
def loglevel(self) -> int:
|
||||||
return CoreConfig.get_config_field(
|
return CoreConfig.str_to_loglevel(
|
||||||
self.__config, "core", "allnet", "ip_check", default=False
|
CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "allnet", "loglevel", default="info"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -218,11 +268,24 @@ class AllnetConfig:
|
|||||||
self.__config, "core", "allnet", "update_cfg_folder", default=""
|
self.__config, "core", "allnet", "update_cfg_folder", default=""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BillingConfig:
|
class BillingConfig:
|
||||||
def __init__(self, parent_config: "CoreConfig") -> None:
|
def __init__(self, parent_config: "CoreConfig") -> None:
|
||||||
self.__config = parent_config
|
self.__config = parent_config
|
||||||
|
|
||||||
|
@property
|
||||||
|
def standalone(self) -> bool:
|
||||||
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "billing", "standalone", default=True
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def loglevel(self) -> int:
|
||||||
|
return CoreConfig.str_to_loglevel(
|
||||||
|
CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "billing", "loglevel", default="info"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def port(self) -> int:
|
def port(self) -> int:
|
||||||
return CoreConfig.get_config_field(
|
return CoreConfig.get_config_field(
|
||||||
@@ -247,11 +310,22 @@ class BillingConfig:
|
|||||||
self.__config, "core", "billing", "signing_key", default="cert/billing.key"
|
self.__config, "core", "billing", "signing_key", default="cert/billing.key"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class AimedbConfig:
|
class AimedbConfig:
|
||||||
def __init__(self, parent_config: "CoreConfig") -> None:
|
def __init__(self, parent_config: "CoreConfig") -> None:
|
||||||
self.__config = parent_config
|
self.__config = parent_config
|
||||||
|
|
||||||
|
@property
|
||||||
|
def enable(self) -> bool:
|
||||||
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "aimedb", "enable", default=True
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def listen_address(self) -> bool:
|
||||||
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "aimedb", "listen_address", default=""
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def loglevel(self) -> int:
|
def loglevel(self) -> int:
|
||||||
return CoreConfig.str_to_loglevel(
|
return CoreConfig.str_to_loglevel(
|
||||||
@@ -272,17 +346,22 @@ class AimedbConfig:
|
|||||||
self.__config, "core", "aimedb", "key", default=""
|
self.__config, "core", "aimedb", "key", default=""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def id_secret(self) -> str:
|
||||||
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "aimedb", "id_secret", default=""
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def id_lifetime_seconds(self) -> int:
|
||||||
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "aimedb", "id_lifetime_seconds", default=86400
|
||||||
|
)
|
||||||
|
|
||||||
class MuchaConfig:
|
class MuchaConfig:
|
||||||
def __init__(self, parent_config: "CoreConfig") -> None:
|
def __init__(self, parent_config: "CoreConfig") -> None:
|
||||||
self.__config = parent_config
|
self.__config = parent_config
|
||||||
|
|
||||||
@property
|
|
||||||
def enable(self) -> int:
|
|
||||||
return CoreConfig.get_config_field(
|
|
||||||
self.__config, "core", "mucha", "enable", default=False
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def loglevel(self) -> int:
|
def loglevel(self) -> int:
|
||||||
return CoreConfig.str_to_loglevel(
|
return CoreConfig.str_to_loglevel(
|
||||||
@@ -291,13 +370,6 @@ class MuchaConfig:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
|
||||||
def hostname(self) -> str:
|
|
||||||
return CoreConfig.get_config_field(
|
|
||||||
self.__config, "core", "mucha", "hostname", default="localhost"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CoreConfig(dict):
|
class CoreConfig(dict):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.server = ServerConfig(self)
|
self.server = ServerConfig(self)
|
||||||
@@ -320,6 +392,19 @@ class CoreConfig(dict):
|
|||||||
else:
|
else:
|
||||||
return logging.INFO
|
return logging.INFO
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def loglevel_to_str(cls, level: int) -> str:
|
||||||
|
if level == logging.ERROR:
|
||||||
|
return "error"
|
||||||
|
elif level == logging.WARN:
|
||||||
|
return "warn"
|
||||||
|
elif level == logging.INFO:
|
||||||
|
return "info"
|
||||||
|
elif level == logging.DEBUG:
|
||||||
|
return "debug"
|
||||||
|
else:
|
||||||
|
return "notset"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_config_field(
|
def get_config_field(
|
||||||
cls, __config: dict, module, *path: str, default: Any = ""
|
cls, __config: dict, module, *path: str, default: Any = ""
|
||||||
|
|||||||
1
core/data/alembic/README
Normal file
1
core/data/alembic/README
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Generic single-database configuration.
|
||||||
64
core/data/alembic/alembic.ini
Normal file
64
core/data/alembic/alembic.ini
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# A generic, single database configuration.
|
||||||
|
|
||||||
|
[alembic]
|
||||||
|
script_location=.
|
||||||
|
|
||||||
|
# template used to generate migration files
|
||||||
|
# file_template = %%(rev)s_%%(slug)s
|
||||||
|
|
||||||
|
# max length of characters to apply to the
|
||||||
|
# "slug" field
|
||||||
|
#truncate_slug_length = 40
|
||||||
|
|
||||||
|
# set to 'true' to run the environment during
|
||||||
|
# the 'revision' command, regardless of autogenerate
|
||||||
|
# revision_environment = false
|
||||||
|
|
||||||
|
# set to 'true' to allow .pyc and .pyo files without
|
||||||
|
# a source .py file to be detected as revisions in the
|
||||||
|
# versions/ directory
|
||||||
|
# sourceless = false
|
||||||
|
|
||||||
|
# version location specification; this defaults
|
||||||
|
# to migrations//versions. When using multiple version
|
||||||
|
# directories, initial revisions must be specified with --version-path
|
||||||
|
# version_locations = %(here)s/bar %(here)s/bat migrations//versions
|
||||||
|
|
||||||
|
# the output encoding used when revision files
|
||||||
|
# are written from script.py.mako
|
||||||
|
# output_encoding = utf-8
|
||||||
|
|
||||||
|
# Logging configuration
|
||||||
|
[loggers]
|
||||||
|
keys = root,sqlalchemy,alembic
|
||||||
|
|
||||||
|
[handlers]
|
||||||
|
keys = console
|
||||||
|
|
||||||
|
[formatters]
|
||||||
|
keys = generic
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level = WARN
|
||||||
|
handlers = console
|
||||||
|
qualname =
|
||||||
|
|
||||||
|
[logger_sqlalchemy]
|
||||||
|
level = WARN
|
||||||
|
handlers =
|
||||||
|
qualname = sqlalchemy.engine
|
||||||
|
|
||||||
|
[logger_alembic]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = alembic
|
||||||
|
|
||||||
|
[handler_console]
|
||||||
|
class = StreamHandler
|
||||||
|
args = (sys.stderr,)
|
||||||
|
level = NOTSET
|
||||||
|
formatter = generic
|
||||||
|
|
||||||
|
[formatter_generic]
|
||||||
|
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||||
|
datefmt = %H:%M:%S
|
||||||
81
core/data/alembic/env.py
Normal file
81
core/data/alembic/env.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
from __future__ import with_statement
|
||||||
|
from alembic import context
|
||||||
|
from sqlalchemy import engine_from_config, pool
|
||||||
|
from logging.config import fileConfig
|
||||||
|
|
||||||
|
from core.data.schema.base import metadata
|
||||||
|
|
||||||
|
# this is the Alembic Config object, which provides
|
||||||
|
# access to the values within the .ini file in use.
|
||||||
|
config = context.config
|
||||||
|
|
||||||
|
# Interpret the config file for Python logging.
|
||||||
|
# This line sets up loggers basically.
|
||||||
|
fileConfig(config.config_file_name)
|
||||||
|
|
||||||
|
# add your model's MetaData object here
|
||||||
|
# for 'autogenerate' support
|
||||||
|
# from myapp import mymodel
|
||||||
|
# target_metadata = mymodel.Base.metadata
|
||||||
|
target_metadata = metadata
|
||||||
|
|
||||||
|
# other values from the config, defined by the needs of env.py,
|
||||||
|
# can be acquired:
|
||||||
|
# my_important_option = config.get_main_option("my_important_option")
|
||||||
|
# ... etc.
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_offline():
|
||||||
|
"""Run migrations in 'offline' mode.
|
||||||
|
|
||||||
|
This configures the context with just a URL
|
||||||
|
and not an Engine, though an Engine is acceptable
|
||||||
|
here as well. By skipping the Engine creation
|
||||||
|
we don't even need a DBAPI to be available.
|
||||||
|
|
||||||
|
Calls to context.execute() here emit the given string to the
|
||||||
|
script output.
|
||||||
|
|
||||||
|
"""
|
||||||
|
raise Exception('Not implemented or configured!')
|
||||||
|
|
||||||
|
url = config.get_main_option("sqlalchemy.url")
|
||||||
|
context.configure(
|
||||||
|
url=url, target_metadata=target_metadata, literal_binds=True)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_online():
|
||||||
|
"""Run migrations in 'online' mode.
|
||||||
|
|
||||||
|
In this scenario we need to create an Engine
|
||||||
|
and associate a connection with the context.
|
||||||
|
|
||||||
|
"""
|
||||||
|
ini_section = config.get_section(config.config_ini_section)
|
||||||
|
overrides = context.get_x_argument(as_dictionary=True)
|
||||||
|
for override in overrides:
|
||||||
|
ini_section[override] = overrides[override]
|
||||||
|
|
||||||
|
connectable = engine_from_config(
|
||||||
|
ini_section,
|
||||||
|
prefix='sqlalchemy.',
|
||||||
|
poolclass=pool.NullPool)
|
||||||
|
|
||||||
|
with connectable.connect() as connection:
|
||||||
|
context.configure(
|
||||||
|
connection=connection,
|
||||||
|
target_metadata=target_metadata,
|
||||||
|
compare_type=True,
|
||||||
|
compare_server_default=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
if context.is_offline_mode():
|
||||||
|
run_migrations_offline()
|
||||||
|
else:
|
||||||
|
run_migrations_online()
|
||||||
24
core/data/alembic/script.py.mako
Normal file
24
core/data/alembic/script.py.mako
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"""${message}
|
||||||
|
|
||||||
|
Revision ID: ${up_revision}
|
||||||
|
Revises: ${down_revision | comma,n}
|
||||||
|
Create Date: ${create_date}
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
${imports if imports else ""}
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = ${repr(up_revision)}
|
||||||
|
down_revision = ${repr(down_revision)}
|
||||||
|
branch_labels = ${repr(branch_labels)}
|
||||||
|
depends_on = ${repr(depends_on)}
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
${upgrades if upgrades else "pass"}
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
${downgrades if downgrades else "pass"}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
"""GekiChu rating tables
|
||||||
|
|
||||||
|
Revision ID: 6a7e8277763b
|
||||||
|
Revises: d8950c7ce2fc
|
||||||
|
Create Date: 2024-03-13 12:18:53.210018
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
from sqlalchemy import Column, Integer, String
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '6a7e8277763b'
|
||||||
|
down_revision = 'd8950c7ce2fc'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
GEKICHU_RATING_TABLE_NAMES = [
|
||||||
|
"chuni_profile_rating",
|
||||||
|
"ongeki_profile_rating",
|
||||||
|
]
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
for table_name in GEKICHU_RATING_TABLE_NAMES:
|
||||||
|
op.create_table(
|
||||||
|
table_name,
|
||||||
|
Column("id", Integer, primary_key=True, nullable=False),
|
||||||
|
Column("user", Integer, nullable=False),
|
||||||
|
Column("version", Integer, nullable=False),
|
||||||
|
Column("type", String(255), nullable=False),
|
||||||
|
Column("index", Integer, nullable=False),
|
||||||
|
Column("musicId", Integer),
|
||||||
|
Column("difficultId", Integer),
|
||||||
|
Column("romVersionCode", Integer),
|
||||||
|
Column("score", Integer),
|
||||||
|
mysql_charset="utf8mb4",
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
None,
|
||||||
|
table_name,
|
||||||
|
"aime_user",
|
||||||
|
["user"],
|
||||||
|
["id"],
|
||||||
|
ondelete="cascade",
|
||||||
|
onupdate="cascade",
|
||||||
|
)
|
||||||
|
op.create_unique_constraint(
|
||||||
|
f"{table_name}_uk",
|
||||||
|
table_name,
|
||||||
|
["user", "version", "type", "index"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
for table_name in GEKICHU_RATING_TABLE_NAMES:
|
||||||
|
op.drop_table(table_name)
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
"""mai2_buddies_support
|
||||||
|
|
||||||
|
Revision ID: 81e44dd6047a
|
||||||
|
Revises: d8950c7ce2fc
|
||||||
|
Create Date: 2024-03-12 19:10:37.063907
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "81e44dd6047a"
|
||||||
|
down_revision = "6a7e8277763b"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.create_table(
|
||||||
|
"mai2_playlog_2p",
|
||||||
|
sa.Column("id", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("user", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("userId1", sa.Integer(), nullable=True),
|
||||||
|
sa.Column("userId2", sa.Integer(), nullable=True),
|
||||||
|
sa.Column("userName1", sa.String(length=25), nullable=True),
|
||||||
|
sa.Column("userName2", sa.String(length=25), nullable=True),
|
||||||
|
sa.Column("regionId", sa.Integer(), nullable=True),
|
||||||
|
sa.Column("placeId", sa.Integer(), nullable=True),
|
||||||
|
sa.Column("user2pPlaylogDetailList", sa.JSON(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["user"], ["aime_user.id"], onupdate="cascade", ondelete="cascade"
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
mysql_charset="utf8mb4",
|
||||||
|
)
|
||||||
|
|
||||||
|
op.add_column(
|
||||||
|
"mai2_playlog",
|
||||||
|
sa.Column(
|
||||||
|
"extBool1", sa.Boolean(), nullable=True, server_default=sa.text("NULL")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
op.add_column(
|
||||||
|
"mai2_profile_detail",
|
||||||
|
sa.Column(
|
||||||
|
"renameCredit", sa.Integer(), nullable=True, server_default=sa.text("NULL")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
op.add_column(
|
||||||
|
"mai2_profile_detail",
|
||||||
|
sa.Column(
|
||||||
|
"currentPlayCount",
|
||||||
|
sa.Integer(),
|
||||||
|
nullable=True,
|
||||||
|
server_default=sa.text("NULL"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.drop_table("mai2_playlog_2p")
|
||||||
|
|
||||||
|
op.drop_column("mai2_playlog", "extBool1")
|
||||||
|
op.drop_column("mai2_profile_detail", "renameCredit")
|
||||||
|
op.drop_column("mai2_profile_detail", "currentPlayCount")
|
||||||
24
core/data/alembic/versions/835b862f9bf0_initial_migration.py
Normal file
24
core/data/alembic/versions/835b862f9bf0_initial_migration.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"""Initial Migration
|
||||||
|
|
||||||
|
Revision ID: 835b862f9bf0
|
||||||
|
Revises:
|
||||||
|
Create Date: 2024-01-09 13:06:10.787432
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '835b862f9bf0'
|
||||||
|
down_revision = None
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
pass
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
"""Remove old db mgmt system
|
||||||
|
|
||||||
|
Revision ID: d8950c7ce2fc
|
||||||
|
Revises: 835b862f9bf0
|
||||||
|
Create Date: 2024-01-09 13:43:51.381175
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'd8950c7ce2fc'
|
||||||
|
down_revision = '835b862f9bf0'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.drop_table("schema_versions")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.create_table(
|
||||||
|
"schema_versions",
|
||||||
|
sa.Column("game", sa.String(4), primary_key=True, nullable=False),
|
||||||
|
sa.Column("version", sa.Integer, nullable=False, server_default="1"),
|
||||||
|
mysql_charset="utf8mb4",
|
||||||
|
)
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
"""IDAC Battle Gift and Tips added
|
||||||
|
|
||||||
|
Revision ID: e4e8d89c9b02
|
||||||
|
Revises: 81e44dd6047a
|
||||||
|
Create Date: 2024-04-01 17:49:50.009718
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "e4e8d89c9b02"
|
||||||
|
down_revision = "81e44dd6047a"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.create_table(
|
||||||
|
"idac_user_battle_gift",
|
||||||
|
sa.Column("id", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("user", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("battle_gift_event_id", sa.Integer(), nullable=True),
|
||||||
|
sa.Column("gift_id", sa.Integer(), nullable=True),
|
||||||
|
sa.Column("gift_status", sa.Integer(), nullable=True),
|
||||||
|
sa.Column(
|
||||||
|
"received_date",
|
||||||
|
sa.TIMESTAMP(),
|
||||||
|
server_default=sa.text("now()"),
|
||||||
|
nullable=True,
|
||||||
|
),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["user"], ["aime_user.id"], onupdate="cascade", ondelete="cascade"
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
sa.UniqueConstraint(
|
||||||
|
"user", "battle_gift_event_id", "gift_id", name="idac_user_battle_gift_uk"
|
||||||
|
),
|
||||||
|
mysql_charset="utf8mb4",
|
||||||
|
)
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
"idac_profile_tips",
|
||||||
|
sa.Column("id", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("user", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("version", sa.Integer(), nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
"tips_list",
|
||||||
|
sa.String(length=16),
|
||||||
|
server_default="QAAAAAAAAAAAAAAA",
|
||||||
|
nullable=True,
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
"timetrial_play_count", sa.Integer(), server_default="0", nullable=True
|
||||||
|
),
|
||||||
|
sa.Column("story_play_count", sa.Integer(), server_default="0", nullable=True),
|
||||||
|
sa.Column(
|
||||||
|
"store_battle_play_count", sa.Integer(), server_default="0", nullable=True
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
"online_battle_play_count", sa.Integer(), server_default="0", nullable=True
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
"special_play_count", sa.Integer(), server_default="0", nullable=True
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
"challenge_play_count", sa.Integer(), server_default="0", nullable=True
|
||||||
|
),
|
||||||
|
sa.Column("theory_play_count", sa.Integer(), server_default="0", nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["user"], ["aime_user.id"], onupdate="cascade", ondelete="cascade"
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
sa.UniqueConstraint("user", "version", name="idac_profile_tips_uk"),
|
||||||
|
mysql_charset="utf8mb4",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.drop_table("idac_user_battle_gift")
|
||||||
|
op.drop_table("idac_profile_tips")
|
||||||
@@ -17,7 +17,7 @@ except ModuleNotFoundError:
|
|||||||
|
|
||||||
def cached(lifetime: int = 10, extra_key: Any = None) -> Callable:
|
def cached(lifetime: int = 10, extra_key: Any = None) -> Callable:
|
||||||
def _cached(func: Callable) -> Callable:
|
def _cached(func: Callable) -> Callable:
|
||||||
if has_mc:
|
if has_mc and (cfg and cfg.database.enable_memcached):
|
||||||
hostname = "127.0.0.1"
|
hostname = "127.0.0.1"
|
||||||
if cfg:
|
if cfg:
|
||||||
hostname = cfg.database.memcached_host
|
hostname = cfg.database.memcached_host
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import logging, coloredlogs
|
import logging, coloredlogs
|
||||||
from typing import Optional, Dict, List
|
from typing import Optional
|
||||||
from sqlalchemy.orm import scoped_session, sessionmaker
|
from sqlalchemy.orm import scoped_session, sessionmaker
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from logging.handlers import TimedRotatingFileHandler
|
from logging.handlers import TimedRotatingFileHandler
|
||||||
import importlib, os
|
import os
|
||||||
import secrets, string
|
import secrets, string
|
||||||
import bcrypt
|
import bcrypt
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
|
import alembic.config
|
||||||
|
import glob
|
||||||
|
|
||||||
from core.config import CoreConfig
|
from core.config import CoreConfig
|
||||||
from core.data.schema import *
|
from core.data.schema import *
|
||||||
@@ -15,7 +16,6 @@ from core.utils import Utils
|
|||||||
|
|
||||||
|
|
||||||
class Data:
|
class Data:
|
||||||
current_schema_version = 6
|
|
||||||
engine = None
|
engine = None
|
||||||
session = None
|
session = None
|
||||||
user = None
|
user = None
|
||||||
@@ -77,281 +77,173 @@ class Data:
|
|||||||
)
|
)
|
||||||
self.logger.handler_set = True # type: ignore
|
self.logger.handler_set = True # type: ignore
|
||||||
|
|
||||||
|
def __alembic_cmd(self, command: str, *args: str) -> None:
|
||||||
|
old_dir = os.path.abspath(os.path.curdir)
|
||||||
|
base_dir = os.path.join(os.path.abspath(os.path.curdir), 'core', 'data', 'alembic')
|
||||||
|
alembicArgs = [
|
||||||
|
"-c",
|
||||||
|
os.path.join(base_dir, "alembic.ini"),
|
||||||
|
"-x",
|
||||||
|
f"script_location={base_dir}",
|
||||||
|
"-x",
|
||||||
|
f"sqlalchemy.url={self.__url}",
|
||||||
|
command,
|
||||||
|
]
|
||||||
|
alembicArgs.extend(args)
|
||||||
|
os.chdir(base_dir)
|
||||||
|
alembic.config.main(argv=alembicArgs)
|
||||||
|
os.chdir(old_dir)
|
||||||
|
|
||||||
def create_database(self):
|
def create_database(self):
|
||||||
self.logger.info("Creating databases...")
|
self.logger.info("Creating databases...")
|
||||||
try:
|
metadata.create_all(
|
||||||
metadata.create_all(self.__engine.connect())
|
self.engine,
|
||||||
except SQLAlchemyError as e:
|
checkfirst=True,
|
||||||
self.logger.error(f"Failed to create databases! {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
games = Utils.get_all_titles()
|
|
||||||
for game_dir, game_mod in games.items():
|
|
||||||
try:
|
|
||||||
if hasattr(game_mod, "database") and hasattr(
|
|
||||||
game_mod, "current_schema_version"
|
|
||||||
):
|
|
||||||
game_mod.database(self.config)
|
|
||||||
metadata.create_all(self.__engine.connect())
|
|
||||||
|
|
||||||
self.base.touch_schema_ver(
|
|
||||||
game_mod.current_schema_version, game_mod.game_codes[0]
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.warning(
|
|
||||||
f"Could not load database schema from {game_dir} - {e}"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.logger.info(f"Setting base_schema_ver to {self.current_schema_version}")
|
|
||||||
self.base.set_schema_ver(self.current_schema_version)
|
|
||||||
|
|
||||||
self.logger.info(
|
|
||||||
f"Setting user auto_incrememnt to {self.config.database.user_table_autoincrement_start}"
|
|
||||||
)
|
|
||||||
self.user.reset_autoincrement(
|
|
||||||
self.config.database.user_table_autoincrement_start
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def recreate_database(self):
|
for _, mod in Utils.get_all_titles().items():
|
||||||
self.logger.info("Dropping all databases...")
|
if hasattr(mod, "database"):
|
||||||
self.base.execute("SET FOREIGN_KEY_CHECKS=0")
|
mod.database(self.config)
|
||||||
try:
|
metadata.create_all(
|
||||||
metadata.drop_all(self.__engine.connect())
|
self.engine,
|
||||||
except SQLAlchemyError as e:
|
checkfirst=True,
|
||||||
self.logger.error(f"Failed to drop databases! {e}")
|
)
|
||||||
return
|
|
||||||
|
|
||||||
for root, dirs, files in os.walk("./titles"):
|
# Stamp the end revision as if alembic had created it, so it can take off after this.
|
||||||
for dir in dirs:
|
self.__alembic_cmd(
|
||||||
if not dir.startswith("__"):
|
"stamp",
|
||||||
try:
|
"head",
|
||||||
mod = importlib.import_module(f"titles.{dir}")
|
)
|
||||||
|
|
||||||
try:
|
def schema_upgrade(self, ver: str = None):
|
||||||
if hasattr(mod, "database"):
|
self.__alembic_cmd(
|
||||||
mod.database(self.config)
|
"upgrade",
|
||||||
metadata.drop_all(self.__engine.connect())
|
"head" if not ver else ver,
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
def schema_downgrade(self, ver: str):
|
||||||
self.logger.warning(
|
self.__alembic_cmd(
|
||||||
f"Could not load database schema from {dir} - {e}"
|
"downgrade",
|
||||||
)
|
ver,
|
||||||
|
)
|
||||||
|
|
||||||
except ImportError as e:
|
async def create_owner(self, email: Optional[str] = None, code: Optional[str] = "00000000000000000000") -> None:
|
||||||
self.logger.warning(
|
|
||||||
f"Failed to load database schema dir {dir} - {e}"
|
|
||||||
)
|
|
||||||
break
|
|
||||||
|
|
||||||
self.base.execute("SET FOREIGN_KEY_CHECKS=1")
|
|
||||||
|
|
||||||
self.create_database()
|
|
||||||
|
|
||||||
def migrate_database(self, game: str, version: Optional[int], action: str) -> None:
|
|
||||||
old_ver = self.base.get_schema_ver(game)
|
|
||||||
sql = ""
|
|
||||||
if version is None:
|
|
||||||
if not game == "CORE":
|
|
||||||
titles = Utils.get_all_titles()
|
|
||||||
|
|
||||||
for folder, mod in titles.items():
|
|
||||||
if not mod.game_codes[0] == game:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if hasattr(mod, "current_schema_version"):
|
|
||||||
version = mod.current_schema_version
|
|
||||||
|
|
||||||
else:
|
|
||||||
self.logger.warning(
|
|
||||||
f"current_schema_version not found for {folder}"
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
version = self.current_schema_version
|
|
||||||
|
|
||||||
if version is None:
|
|
||||||
self.logger.warning(
|
|
||||||
f"Could not determine latest version for {game}, please specify --version"
|
|
||||||
)
|
|
||||||
|
|
||||||
if old_ver is None:
|
|
||||||
self.logger.error(
|
|
||||||
f"Schema for game {game} does not exist, did you run the creation script?"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
if old_ver == version:
|
|
||||||
self.logger.info(
|
|
||||||
f"Schema for game {game} is already version {old_ver}, nothing to do"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
if action == "upgrade":
|
|
||||||
for x in range(old_ver, version):
|
|
||||||
if not os.path.exists(
|
|
||||||
f"core/data/schema/versions/{game.upper()}_{x + 1}_{action}.sql"
|
|
||||||
):
|
|
||||||
self.logger.error(
|
|
||||||
f"Could not find {action} script {game.upper()}_{x + 1}_{action}.sql in core/data/schema/versions folder"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
with open(
|
|
||||||
f"core/data/schema/versions/{game.upper()}_{x + 1}_{action}.sql",
|
|
||||||
"r",
|
|
||||||
encoding="utf-8",
|
|
||||||
) as f:
|
|
||||||
sql = f.read()
|
|
||||||
|
|
||||||
result = self.base.execute(sql)
|
|
||||||
if result is None:
|
|
||||||
self.logger.error("Error execuing sql script!")
|
|
||||||
return None
|
|
||||||
|
|
||||||
else:
|
|
||||||
for x in range(old_ver, version, -1):
|
|
||||||
if not os.path.exists(
|
|
||||||
f"core/data/schema/versions/{game.upper()}_{x - 1}_{action}.sql"
|
|
||||||
):
|
|
||||||
self.logger.error(
|
|
||||||
f"Could not find {action} script {game.upper()}_{x - 1}_{action}.sql in core/data/schema/versions folder"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
with open(
|
|
||||||
f"core/data/schema/versions/{game.upper()}_{x - 1}_{action}.sql",
|
|
||||||
"r",
|
|
||||||
encoding="utf-8",
|
|
||||||
) as f:
|
|
||||||
sql = f.read()
|
|
||||||
|
|
||||||
result = self.base.execute(sql)
|
|
||||||
if result is None:
|
|
||||||
self.logger.error("Error execuing sql script!")
|
|
||||||
return None
|
|
||||||
|
|
||||||
result = self.base.set_schema_ver(version, game)
|
|
||||||
if result is None:
|
|
||||||
self.logger.error("Error setting version in schema_version table!")
|
|
||||||
return None
|
|
||||||
|
|
||||||
self.logger.info(f"Successfully migrated {game} to schema version {version}")
|
|
||||||
|
|
||||||
def create_owner(self, email: Optional[str] = None) -> None:
|
|
||||||
pw = "".join(
|
pw = "".join(
|
||||||
secrets.choice(string.ascii_letters + string.digits) for i in range(20)
|
secrets.choice(string.ascii_letters + string.digits) for i in range(20)
|
||||||
)
|
)
|
||||||
hash = bcrypt.hashpw(pw.encode(), bcrypt.gensalt())
|
hash = bcrypt.hashpw(pw.encode(), bcrypt.gensalt())
|
||||||
|
|
||||||
user_id = self.user.create_user(email=email, permission=255, password=hash)
|
user_id = await self.user.create_user(username="sysowner", email=email, password=hash.decode(), permission=255)
|
||||||
if user_id is None:
|
if user_id is None:
|
||||||
self.logger.error(f"Failed to create owner with email {email}")
|
self.logger.error(f"Failed to create owner with email {email}")
|
||||||
return
|
return
|
||||||
|
|
||||||
card_id = self.card.create_card(user_id, "00000000000000000000")
|
card_id = await self.card.create_card(user_id, code)
|
||||||
if card_id is None:
|
if card_id is None:
|
||||||
self.logger.error(f"Failed to create card for owner with id {user_id}")
|
self.logger.error(f"Failed to create card for owner with id {user_id}")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
f"Successfully created owner with email {email}, access code 00000000000000000000, and password {pw} Make sure to change this password and assign a real card ASAP!"
|
f"Successfully created owner with email {email}, access code {code}, and password {pw} Make sure to change this password and assign a real card ASAP!"
|
||||||
)
|
)
|
||||||
|
|
||||||
def migrate_card(self, old_ac: str, new_ac: str, should_force: bool) -> None:
|
async def migrate(self) -> None:
|
||||||
if old_ac == new_ac:
|
exist = await self.base.execute("SELECT * FROM alembic_version")
|
||||||
self.logger.error("Both access codes are the same!")
|
if exist is not None:
|
||||||
|
self.logger.warn("No need to migrate as you have already migrated to alembic. If you are trying to upgrade the schema, use `upgrade` instead!")
|
||||||
return
|
return
|
||||||
|
|
||||||
new_card = self.card.get_card_by_access_code(new_ac)
|
self.logger.info("Upgrading to latest with legacy system")
|
||||||
if new_card is None:
|
if not await self.legacy_upgrade():
|
||||||
self.card.update_access_code(old_ac, new_ac)
|
self.logger.warn("No need to migrate as you have already deleted the old schema_versions system. If you are trying to upgrade the schema, use `upgrade` instead!")
|
||||||
return
|
return
|
||||||
|
self.logger.info("Done")
|
||||||
|
|
||||||
if not should_force:
|
self.logger.info("Stamp with initial revision")
|
||||||
self.logger.warning(
|
self.__alembic_cmd(
|
||||||
f"Card already exists for access code {new_ac} (id {new_card['id']}). If you wish to continue, rerun with the '--force' flag."
|
"stamp",
|
||||||
f" All exiting data on the target card {new_ac} will be perminently erased and replaced with data from card {old_ac}."
|
"835b862f9bf0",
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
self.logger.info(
|
|
||||||
f"All exiting data on the target card {new_ac} will be perminently erased and replaced with data from card {old_ac}."
|
|
||||||
)
|
)
|
||||||
self.card.delete_card(new_card["id"])
|
|
||||||
self.card.update_access_code(old_ac, new_ac)
|
|
||||||
|
|
||||||
hanging_user = self.user.get_user(new_card["user"])
|
self.logger.info("Upgrade")
|
||||||
if hanging_user["password"] is None:
|
self.__alembic_cmd(
|
||||||
self.logger.info(f"Delete hanging user {hanging_user['id']}")
|
"upgrade",
|
||||||
self.user.delete_user(hanging_user["id"])
|
"head",
|
||||||
|
)
|
||||||
|
|
||||||
def delete_hanging_users(self) -> None:
|
async def legacy_upgrade(self) -> bool:
|
||||||
"""
|
vers = await self.base.execute("SELECT * FROM schema_versions")
|
||||||
Finds and deletes users that have not registered for the webui that have no cards assocated with them.
|
if vers is None:
|
||||||
"""
|
self.logger.warn("Cannot legacy upgrade, schema_versions table unavailable!")
|
||||||
unreg_users = self.user.get_unregistered_users()
|
return False
|
||||||
if unreg_users is None:
|
|
||||||
self.logger.error("Error occoured finding unregistered users")
|
|
||||||
|
|
||||||
for user in unreg_users:
|
db_vers = {}
|
||||||
cards = self.card.get_user_cards(user["id"])
|
vers_list = vers.fetchall()
|
||||||
if cards is None:
|
for x in vers_list:
|
||||||
self.logger.error(f"Error getting cards for user {user['id']}")
|
db_vers[x['game']] = x['version']
|
||||||
continue
|
|
||||||
|
|
||||||
if not cards:
|
core_now_ver = int(db_vers['CORE']) + 1
|
||||||
self.logger.info(f"Delete hanging user {user['id']}")
|
while os.path.exists(f"core/data/schema/versions/CORE_{core_now_ver}_upgrade.sql"):
|
||||||
self.user.delete_user(user["id"])
|
with open(f"core/data/schema/versions/CORE_{core_now_ver}_upgrade.sql", "r") as f:
|
||||||
|
result = await self.base.execute(f.read())
|
||||||
|
|
||||||
def autoupgrade(self) -> None:
|
if result is None:
|
||||||
all_game_versions = self.base.get_all_schema_vers()
|
self.logger.error(f"Invalid upgrade script CORE_{core_now_ver}_upgrade.sql")
|
||||||
if all_game_versions is None:
|
break
|
||||||
self.logger.warning("Failed to get schema versions")
|
|
||||||
|
result = await self.base.execute(f"UPDATE schema_versions SET version = {core_now_ver} WHERE game = 'CORE'")
|
||||||
|
if result is None:
|
||||||
|
self.logger.error(f"Failed to update schema version for CORE to {core_now_ver}")
|
||||||
|
break
|
||||||
|
|
||||||
|
self.logger.info(f"Upgrade CORE to version {core_now_ver}")
|
||||||
|
core_now_ver += 1
|
||||||
|
|
||||||
|
for _, mod in Utils.get_all_titles().items():
|
||||||
|
game_codes = getattr(mod, "game_codes", [])
|
||||||
|
for game in game_codes:
|
||||||
|
if game not in db_vers:
|
||||||
|
self.logger.warn(f"{game} does not have an antry in schema_versions, skipping")
|
||||||
|
continue
|
||||||
|
|
||||||
|
now_ver = int(db_vers[game]) + 1
|
||||||
|
while os.path.exists(f"core/data/schema/versions/{game}_{now_ver}_upgrade.sql"):
|
||||||
|
with open(f"core/data/schema/versions/{game}_{now_ver}_upgrade.sql", "r") as f:
|
||||||
|
result = await self.base.execute(f.read())
|
||||||
|
|
||||||
|
if result is None:
|
||||||
|
self.logger.error(f"Invalid upgrade script {game}_{now_ver}_upgrade.sql")
|
||||||
|
break
|
||||||
|
|
||||||
|
result = await self.base.execute(f"UPDATE schema_versions SET version = {now_ver} WHERE game = '{game}'")
|
||||||
|
if result is None:
|
||||||
|
self.logger.error(f"Failed to update schema version for {game} to {now_ver}")
|
||||||
|
break
|
||||||
|
|
||||||
|
self.logger.info(f"Upgrade {game} to version {now_ver}")
|
||||||
|
now_ver += 1
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def create_revision(self, message: str) -> None:
|
||||||
|
if not message:
|
||||||
|
self.logger.info("Message is required for create-revision")
|
||||||
return
|
return
|
||||||
|
|
||||||
all_games = Utils.get_all_titles()
|
self.__alembic_cmd(
|
||||||
all_games_list: Dict[str, int] = {}
|
"revision",
|
||||||
for _, mod in all_games.items():
|
"-m",
|
||||||
if hasattr(mod, "current_schema_version"):
|
message,
|
||||||
all_games_list[mod.game_codes[0]] = mod.current_schema_version
|
)
|
||||||
|
|
||||||
for x in all_game_versions:
|
async def create_revision_auto(self, message: str) -> None:
|
||||||
failed = False
|
if not message:
|
||||||
game = x["game"].upper()
|
self.logger.info("Message is required for create-revision")
|
||||||
update_ver = int(x["version"])
|
return
|
||||||
latest_ver = all_games_list.get(game, 1)
|
|
||||||
if game == "CORE":
|
|
||||||
latest_ver = self.current_schema_version
|
|
||||||
|
|
||||||
if update_ver == latest_ver:
|
self.__alembic_cmd(
|
||||||
self.logger.info(f"{game} is already latest version")
|
"revision",
|
||||||
continue
|
"--autogenerate",
|
||||||
|
"-m",
|
||||||
for y in range(update_ver + 1, latest_ver + 1):
|
message,
|
||||||
if os.path.exists(f"core/data/schema/versions/{game}_{y}_upgrade.sql"):
|
)
|
||||||
with open(
|
|
||||||
f"core/data/schema/versions/{game}_{y}_upgrade.sql",
|
|
||||||
"r",
|
|
||||||
encoding="utf-8",
|
|
||||||
) as f:
|
|
||||||
sql = f.read()
|
|
||||||
|
|
||||||
result = self.base.execute(sql)
|
|
||||||
if result is None:
|
|
||||||
self.logger.error(
|
|
||||||
f"Error execuing sql script for game {game} v{y}!"
|
|
||||||
)
|
|
||||||
failed = True
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
self.logger.warning(f"Could not find script {game}_{y}_upgrade.sql")
|
|
||||||
failed = True
|
|
||||||
|
|
||||||
if not failed:
|
|
||||||
self.base.set_schema_ver(latest_ver, game)
|
|
||||||
|
|
||||||
def show_versions(self) -> None:
|
|
||||||
all_game_versions = self.base.get_all_schema_vers()
|
|
||||||
for ver in all_game_versions:
|
|
||||||
self.logger.info(f"{ver['game']} -> v{ver['version']}")
|
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ machine = Table(
|
|||||||
Column("country", String(3)), # overwrites if not null
|
Column("country", String(3)), # overwrites if not null
|
||||||
Column("timezone", String(255)),
|
Column("timezone", String(255)),
|
||||||
Column("ota_enable", Boolean),
|
Column("ota_enable", Boolean),
|
||||||
Column("is_cab", Boolean),
|
|
||||||
Column("memo", String(255)),
|
Column("memo", String(255)),
|
||||||
Column("is_cab", Boolean),
|
Column("is_cab", Boolean),
|
||||||
Column("data", JSON),
|
Column("data", JSON),
|
||||||
@@ -70,7 +69,7 @@ arcade_owner = Table(
|
|||||||
|
|
||||||
|
|
||||||
class ArcadeData(BaseData):
|
class ArcadeData(BaseData):
|
||||||
def get_machine(self, serial: str = None, id: int = None) -> Optional[Row]:
|
async def get_machine(self, serial: str = None, id: int = None) -> Optional[Row]:
|
||||||
if serial is not None:
|
if serial is not None:
|
||||||
serial = serial.replace("-", "")
|
serial = serial.replace("-", "")
|
||||||
if len(serial) == 11:
|
if len(serial) == 11:
|
||||||
@@ -90,12 +89,12 @@ class ArcadeData(BaseData):
|
|||||||
self.logger.error(f"{__name__ }: Need either serial or ID to look up!")
|
self.logger.error(f"{__name__ }: Need either serial or ID to look up!")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
result = self.execute(sql)
|
result = await self.execute(sql)
|
||||||
if result is None:
|
if result is None:
|
||||||
return None
|
return None
|
||||||
return result.fetchone()
|
return result.fetchone()
|
||||||
|
|
||||||
def put_machine(
|
async def create_machine(
|
||||||
self,
|
self,
|
||||||
arcade_id: int,
|
arcade_id: int,
|
||||||
serial: str = "",
|
serial: str = "",
|
||||||
@@ -103,21 +102,21 @@ class ArcadeData(BaseData):
|
|||||||
game: str = None,
|
game: str = None,
|
||||||
is_cab: bool = False,
|
is_cab: bool = False,
|
||||||
) -> Optional[int]:
|
) -> Optional[int]:
|
||||||
if arcade_id:
|
if not arcade_id:
|
||||||
self.logger.error(f"{__name__ }: Need arcade id!")
|
self.logger.error(f"{__name__ }: Need arcade id!")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
sql = machine.insert().values(
|
sql = machine.insert().values(
|
||||||
arcade=arcade_id, keychip=serial, board=board, game=game, is_cab=is_cab
|
arcade=arcade_id, serial=serial, board=board, game=game, is_cab=is_cab
|
||||||
)
|
)
|
||||||
|
|
||||||
result = self.execute(sql)
|
result = await self.execute(sql)
|
||||||
if result is None:
|
if result is None:
|
||||||
return None
|
return None
|
||||||
return result.lastrowid
|
return result.lastrowid
|
||||||
|
|
||||||
def set_machine_serial(self, machine_id: int, serial: str) -> None:
|
async def set_machine_serial(self, machine_id: int, serial: str) -> None:
|
||||||
result = self.execute(
|
result = await self.execute(
|
||||||
machine.update(machine.c.id == machine_id).values(keychip=serial)
|
machine.update(machine.c.id == machine_id).values(keychip=serial)
|
||||||
)
|
)
|
||||||
if result is None:
|
if result is None:
|
||||||
@@ -126,8 +125,8 @@ class ArcadeData(BaseData):
|
|||||||
)
|
)
|
||||||
return result.lastrowid
|
return result.lastrowid
|
||||||
|
|
||||||
def set_machine_boardid(self, machine_id: int, boardid: str) -> None:
|
async def set_machine_boardid(self, machine_id: int, boardid: str) -> None:
|
||||||
result = self.execute(
|
result = await self.execute(
|
||||||
machine.update(machine.c.id == machine_id).values(board=boardid)
|
machine.update(machine.c.id == machine_id).values(board=boardid)
|
||||||
)
|
)
|
||||||
if result is None:
|
if result is None:
|
||||||
@@ -135,29 +134,29 @@ class ArcadeData(BaseData):
|
|||||||
f"Failed to update board id for machine {machine_id} -> {boardid}"
|
f"Failed to update board id for machine {machine_id} -> {boardid}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_arcade(self, id: int) -> Optional[Row]:
|
async def get_arcade(self, id: int) -> Optional[Row]:
|
||||||
sql = arcade.select(arcade.c.id == id)
|
sql = arcade.select(arcade.c.id == id)
|
||||||
result = self.execute(sql)
|
result = await self.execute(sql)
|
||||||
if result is None:
|
if result is None:
|
||||||
return None
|
return None
|
||||||
return result.fetchone()
|
return result.fetchone()
|
||||||
|
|
||||||
def get_arcade_machines(self, id: int) -> Optional[List[Row]]:
|
async def get_arcade_machines(self, id: int) -> Optional[List[Row]]:
|
||||||
sql = machine.select(machine.c.arcade == id)
|
sql = machine.select(machine.c.arcade == id)
|
||||||
result = self.execute(sql)
|
result = await self.execute(sql)
|
||||||
if result is None:
|
if result is None:
|
||||||
return None
|
return None
|
||||||
return result.fetchall()
|
return result.fetchall()
|
||||||
|
|
||||||
def put_arcade(
|
async def create_arcade(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str = None,
|
||||||
nickname: str = None,
|
nickname: str = None,
|
||||||
country: str = "JPN",
|
country: str = "JPN",
|
||||||
country_id: int = 1,
|
country_id: int = 1,
|
||||||
state: str = "",
|
state: str = "",
|
||||||
city: str = "",
|
city: str = "",
|
||||||
regional_id: int = 1,
|
region_id: int = 1,
|
||||||
) -> Optional[int]:
|
) -> Optional[int]:
|
||||||
if nickname is None:
|
if nickname is None:
|
||||||
nickname = name
|
nickname = name
|
||||||
@@ -169,46 +168,46 @@ class ArcadeData(BaseData):
|
|||||||
country_id=country_id,
|
country_id=country_id,
|
||||||
state=state,
|
state=state,
|
||||||
city=city,
|
city=city,
|
||||||
regional_id=regional_id,
|
region_id=region_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
result = self.execute(sql)
|
result = await self.execute(sql)
|
||||||
if result is None:
|
if result is None:
|
||||||
return None
|
return None
|
||||||
return result.lastrowid
|
return result.lastrowid
|
||||||
|
|
||||||
def get_arcades_managed_by_user(self, user_id: int) -> Optional[List[Row]]:
|
async def get_arcades_managed_by_user(self, user_id: int) -> Optional[List[Row]]:
|
||||||
sql = select(arcade).join(arcade_owner, arcade_owner.c.arcade == arcade.c.id).where(arcade_owner.c.user == user_id)
|
sql = select(arcade).join(arcade_owner, arcade_owner.c.arcade == arcade.c.id).where(arcade_owner.c.user == user_id)
|
||||||
result = self.execute(sql)
|
result = await self.execute(sql)
|
||||||
if result is None:
|
if result is None:
|
||||||
return False
|
return False
|
||||||
return result.fetchall()
|
return result.fetchall()
|
||||||
|
|
||||||
def get_manager_permissions(self, user_id: int, arcade_id: int) -> Optional[int]:
|
async def get_manager_permissions(self, user_id: int, arcade_id: int) -> Optional[int]:
|
||||||
sql = select(arcade_owner.c.permissions).where(and_(arcade_owner.c.user == user_id, arcade_owner.c.arcade == arcade_id))
|
sql = select(arcade_owner.c.permissions).where(and_(arcade_owner.c.user == user_id, arcade_owner.c.arcade == arcade_id))
|
||||||
result = self.execute(sql)
|
result = await self.execute(sql)
|
||||||
if result is None:
|
if result is None:
|
||||||
return False
|
return False
|
||||||
return result.fetchone()
|
return result.fetchone()
|
||||||
|
|
||||||
def get_arcade_owners(self, arcade_id: int) -> Optional[Row]:
|
async def get_arcade_owners(self, arcade_id: int) -> Optional[Row]:
|
||||||
sql = select(arcade_owner).where(arcade_owner.c.arcade == arcade_id)
|
sql = select(arcade_owner).where(arcade_owner.c.arcade == arcade_id)
|
||||||
|
|
||||||
result = self.execute(sql)
|
result = await self.execute(sql)
|
||||||
if result is None:
|
if result is None:
|
||||||
return None
|
return None
|
||||||
return result.fetchall()
|
return result.fetchall()
|
||||||
|
|
||||||
def add_arcade_owner(self, arcade_id: int, user_id: int) -> None:
|
async def add_arcade_owner(self, arcade_id: int, user_id: int) -> None:
|
||||||
sql = insert(arcade_owner).values(arcade=arcade_id, user=user_id)
|
sql = insert(arcade_owner).values(arcade=arcade_id, user=user_id)
|
||||||
|
|
||||||
result = self.execute(sql)
|
result = await self.execute(sql)
|
||||||
if result is None:
|
if result is None:
|
||||||
return None
|
return None
|
||||||
return result.lastrowid
|
return result.lastrowid
|
||||||
|
|
||||||
def format_serial(
|
def format_serial( # TODO: Actual serial stuff
|
||||||
self, platform_code: str, platform_rev: int, serial_num: int, append: int = 4152
|
self, platform_code: str, platform_rev: int, serial_num: int, append: int = 8888
|
||||||
) -> str:
|
) -> str:
|
||||||
return f"{platform_code}{platform_rev:02d}A{serial_num:04d}{append:04d}" # 0x41 = A, 0x52 = R
|
return f"{platform_code}{platform_rev:02d}A{serial_num:04d}{append:04d}" # 0x41 = A, 0x52 = R
|
||||||
|
|
||||||
@@ -218,9 +217,16 @@ class ArcadeData(BaseData):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def find_arcade_by_name(self, name: str) -> List[Row]:
|
async def get_arcade_by_name(self, name: str) -> Optional[List[Row]]:
|
||||||
sql = arcade.select(or_(arcade.c.name.like(f"%{name}%"), arcade.c.nickname.like(f"%{name}%")))
|
sql = arcade.select(or_(arcade.c.name.like(f"%{name}%"), arcade.c.nickname.like(f"%{name}%")))
|
||||||
result = self.execute(sql)
|
result = await self.execute(sql)
|
||||||
if result is None:
|
if result is None:
|
||||||
return False
|
return None
|
||||||
|
return result.fetchall()
|
||||||
|
|
||||||
|
async def get_arcades_by_ip(self, ip: str) -> Optional[List[Row]]:
|
||||||
|
sql = arcade.select().where(arcade.c.ip == ip)
|
||||||
|
result = await self.execute(sql)
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
return result.fetchall()
|
return result.fetchall()
|
||||||
|
|||||||
@@ -15,14 +15,6 @@ from core.config import CoreConfig
|
|||||||
|
|
||||||
metadata = MetaData()
|
metadata = MetaData()
|
||||||
|
|
||||||
schema_ver = Table(
|
|
||||||
"schema_versions",
|
|
||||||
metadata,
|
|
||||||
Column("game", String(4), primary_key=True, nullable=False),
|
|
||||||
Column("version", Integer, nullable=False, server_default="1"),
|
|
||||||
mysql_charset="utf8mb4",
|
|
||||||
)
|
|
||||||
|
|
||||||
event_log = Table(
|
event_log = Table(
|
||||||
"event_log",
|
"event_log",
|
||||||
metadata,
|
metadata,
|
||||||
@@ -43,11 +35,11 @@ class BaseData:
|
|||||||
self.conn = conn
|
self.conn = conn
|
||||||
self.logger = logging.getLogger("database")
|
self.logger = logging.getLogger("database")
|
||||||
|
|
||||||
def execute(self, sql: str, opts: Dict[str, Any] = {}) -> Optional[CursorResult]:
|
async def execute(self, sql: str, opts: Dict[str, Any] = {}) -> Optional[CursorResult]:
|
||||||
res = None
|
res = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.logger.info(f"SQL Execute: {''.join(str(sql).splitlines())}")
|
self.logger.debug(f"SQL Execute: {''.join(str(sql).splitlines())}")
|
||||||
res = self.conn.execute(text(sql), opts)
|
res = self.conn.execute(text(sql), opts)
|
||||||
|
|
||||||
except SQLAlchemyError as e:
|
except SQLAlchemyError as e:
|
||||||
@@ -82,52 +74,7 @@ class BaseData:
|
|||||||
"""
|
"""
|
||||||
return randrange(10000, 9999999)
|
return randrange(10000, 9999999)
|
||||||
|
|
||||||
def get_all_schema_vers(self) -> Optional[List[Row]]:
|
async def log_event(
|
||||||
sql = select(schema_ver)
|
|
||||||
|
|
||||||
result = self.execute(sql)
|
|
||||||
if result is None:
|
|
||||||
return None
|
|
||||||
return result.fetchall()
|
|
||||||
|
|
||||||
def get_schema_ver(self, game: str) -> Optional[int]:
|
|
||||||
sql = select(schema_ver).where(schema_ver.c.game == game)
|
|
||||||
|
|
||||||
result = self.execute(sql)
|
|
||||||
if result is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
row = result.fetchone()
|
|
||||||
if row is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return row["version"]
|
|
||||||
|
|
||||||
def touch_schema_ver(self, ver: int, game: str = "CORE") -> Optional[int]:
|
|
||||||
sql = insert(schema_ver).values(game=game, version=ver)
|
|
||||||
conflict = sql.on_duplicate_key_update(version=schema_ver.c.version)
|
|
||||||
|
|
||||||
result = self.execute(conflict)
|
|
||||||
if result is None:
|
|
||||||
self.logger.error(
|
|
||||||
f"Failed to update schema version for game {game} (v{ver})"
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
return result.lastrowid
|
|
||||||
|
|
||||||
def set_schema_ver(self, ver: int, game: str = "CORE") -> Optional[int]:
|
|
||||||
sql = insert(schema_ver).values(game=game, version=ver)
|
|
||||||
conflict = sql.on_duplicate_key_update(version=ver)
|
|
||||||
|
|
||||||
result = self.execute(conflict)
|
|
||||||
if result is None:
|
|
||||||
self.logger.error(
|
|
||||||
f"Failed to update schema version for game {game} (v{ver})"
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
return result.lastrowid
|
|
||||||
|
|
||||||
def log_event(
|
|
||||||
self, system: str, type: str, severity: int, message: str, details: Dict = {}
|
self, system: str, type: str, severity: int, message: str, details: Dict = {}
|
||||||
) -> Optional[int]:
|
) -> Optional[int]:
|
||||||
sql = event_log.insert().values(
|
sql = event_log.insert().values(
|
||||||
@@ -137,7 +84,7 @@ class BaseData:
|
|||||||
message=message,
|
message=message,
|
||||||
details=json.dumps(details),
|
details=json.dumps(details),
|
||||||
)
|
)
|
||||||
result = self.execute(sql)
|
result = await self.execute(sql)
|
||||||
|
|
||||||
if result is None:
|
if result is None:
|
||||||
self.logger.error(
|
self.logger.error(
|
||||||
@@ -147,9 +94,9 @@ class BaseData:
|
|||||||
|
|
||||||
return result.lastrowid
|
return result.lastrowid
|
||||||
|
|
||||||
def get_event_log(self, entries: int = 100) -> Optional[List[Dict]]:
|
async def get_event_log(self, entries: int = 100) -> Optional[List[Dict]]:
|
||||||
sql = event_log.select().limit(entries).all()
|
sql = event_log.select().limit(entries).all()
|
||||||
result = self.execute(sql)
|
result = await self.execute(sql)
|
||||||
|
|
||||||
if result is None:
|
if result is None:
|
||||||
return None
|
return None
|
||||||
@@ -157,6 +104,8 @@ class BaseData:
|
|||||||
|
|
||||||
def fix_bools(self, data: Dict) -> Dict:
|
def fix_bools(self, data: Dict) -> Dict:
|
||||||
for k, v in data.items():
|
for k, v in data.items():
|
||||||
|
if k == "userName" or k == "teamName":
|
||||||
|
continue
|
||||||
if type(v) == str and v.lower() == "true":
|
if type(v) == str and v.lower() == "true":
|
||||||
data[k] = True
|
data[k] = True
|
||||||
elif type(v) == str and v.lower() == "false":
|
elif type(v) == str and v.lower() == "false":
|
||||||
|
|||||||
@@ -27,70 +27,101 @@ aime_card = Table(
|
|||||||
|
|
||||||
|
|
||||||
class CardData(BaseData):
|
class CardData(BaseData):
|
||||||
def get_card_by_access_code(self, access_code: str) -> Optional[Row]:
|
async def get_card_by_access_code(self, access_code: str) -> Optional[Row]:
|
||||||
sql = aime_card.select(aime_card.c.access_code == access_code)
|
sql = aime_card.select(aime_card.c.access_code == access_code)
|
||||||
|
|
||||||
result = self.execute(sql)
|
result = await self.execute(sql)
|
||||||
if result is None:
|
if result is None:
|
||||||
return None
|
return None
|
||||||
return result.fetchone()
|
return result.fetchone()
|
||||||
|
|
||||||
def get_card_by_id(self, card_id: int) -> Optional[Row]:
|
async def get_card_by_id(self, card_id: int) -> Optional[Row]:
|
||||||
sql = aime_card.select(aime_card.c.id == card_id)
|
sql = aime_card.select(aime_card.c.id == card_id)
|
||||||
|
|
||||||
result = self.execute(sql)
|
result = await self.execute(sql)
|
||||||
if result is None:
|
if result is None:
|
||||||
return None
|
return None
|
||||||
return result.fetchone()
|
return result.fetchone()
|
||||||
|
|
||||||
def update_access_code(self, old_ac: str, new_ac: str) -> None:
|
async def update_access_code(self, old_ac: str, new_ac: str) -> None:
|
||||||
sql = aime_card.update(aime_card.c.access_code == old_ac).values(
|
sql = aime_card.update(aime_card.c.access_code == old_ac).values(
|
||||||
access_code=new_ac
|
access_code=new_ac
|
||||||
)
|
)
|
||||||
|
|
||||||
result = self.execute(sql)
|
result = await self.execute(sql)
|
||||||
if result is None:
|
if result is None:
|
||||||
self.logger.error(
|
self.logger.error(
|
||||||
f"Failed to change card access code from {old_ac} to {new_ac}"
|
f"Failed to change card access code from {old_ac} to {new_ac}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_user_id_from_card(self, access_code: str) -> Optional[int]:
|
async def get_user_id_from_card(self, access_code: str) -> Optional[int]:
|
||||||
"""
|
"""
|
||||||
Given a 20 digit access code as a string, get the user id associated with that card
|
Given a 20 digit access code as a string, get the user id associated with that card
|
||||||
"""
|
"""
|
||||||
card = self.get_card_by_access_code(access_code)
|
card = await self.get_card_by_access_code(access_code)
|
||||||
if card is None:
|
if card is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return int(card["user"])
|
return int(card["user"])
|
||||||
|
|
||||||
def delete_card(self, card_id: int) -> None:
|
async def get_card_banned(self, access_code: str) -> Optional[bool]:
|
||||||
|
"""
|
||||||
|
Given a 20 digit access code as a string, check if the card is banned
|
||||||
|
"""
|
||||||
|
card = await self.get_card_by_access_code(access_code)
|
||||||
|
if card is None:
|
||||||
|
return None
|
||||||
|
if card["is_banned"]:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def get_card_locked(self, access_code: str) -> Optional[bool]:
|
||||||
|
"""
|
||||||
|
Given a 20 digit access code as a string, check if the card is locked
|
||||||
|
"""
|
||||||
|
card = await self.get_card_by_access_code(access_code)
|
||||||
|
if card is None:
|
||||||
|
return None
|
||||||
|
if card["is_locked"]:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def delete_card(self, card_id: int) -> None:
|
||||||
sql = aime_card.delete(aime_card.c.id == card_id)
|
sql = aime_card.delete(aime_card.c.id == card_id)
|
||||||
|
|
||||||
result = self.execute(sql)
|
result = await self.execute(sql)
|
||||||
if result is None:
|
if result is None:
|
||||||
self.logger.error(f"Failed to delete card with id {card_id}")
|
self.logger.error(f"Failed to delete card with id {card_id}")
|
||||||
|
|
||||||
def get_user_cards(self, aime_id: int) -> Optional[List[Row]]:
|
async def get_user_cards(self, aime_id: int) -> Optional[List[Row]]:
|
||||||
"""
|
"""
|
||||||
Returns all cards owned by a user
|
Returns all cards owned by a user
|
||||||
"""
|
"""
|
||||||
sql = aime_card.select(aime_card.c.user == aime_id)
|
sql = aime_card.select(aime_card.c.user == aime_id)
|
||||||
result = self.execute(sql)
|
result = await self.execute(sql)
|
||||||
if result is None:
|
if result is None:
|
||||||
return None
|
return None
|
||||||
return result.fetchall()
|
return result.fetchall()
|
||||||
|
|
||||||
def create_card(self, user_id: int, access_code: str) -> Optional[int]:
|
async def create_card(self, user_id: int, access_code: str) -> Optional[int]:
|
||||||
"""
|
"""
|
||||||
Given a aime_user id and a 20 digit access code as a string, create a card and return the ID if successful
|
Given a aime_user id and a 20 digit access code as a string, create a card and return the ID if successful
|
||||||
"""
|
"""
|
||||||
sql = aime_card.insert().values(user=user_id, access_code=access_code)
|
sql = aime_card.insert().values(user=user_id, access_code=access_code)
|
||||||
result = self.execute(sql)
|
result = await self.execute(sql)
|
||||||
if result is None:
|
if result is None:
|
||||||
return None
|
return None
|
||||||
return result.lastrowid
|
return result.lastrowid
|
||||||
|
|
||||||
|
async def update_card_last_login(self, access_code: str) -> None:
|
||||||
|
sql = aime_card.update(aime_card.c.access_code == access_code).values(
|
||||||
|
last_login_date=func.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await self.execute(sql)
|
||||||
|
if result is None:
|
||||||
|
self.logger.warn(f"Failed to update last login time for {access_code}")
|
||||||
|
|
||||||
def to_access_code(self, luid: str) -> str:
|
def to_access_code(self, luid: str) -> str:
|
||||||
"""
|
"""
|
||||||
Given a felica cards internal 16 hex character luid, convert it to a 0-padded 20 digit access code as a string
|
Given a felica cards internal 16 hex character luid, convert it to a 0-padded 20 digit access code as a string
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
from enum import Enum
|
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
from sqlalchemy import Table, Column
|
from sqlalchemy import Table, Column
|
||||||
from sqlalchemy.types import Integer, String, TIMESTAMP
|
from sqlalchemy.types import Integer, String, TIMESTAMP
|
||||||
@@ -24,15 +23,8 @@ aime_user = Table(
|
|||||||
mysql_charset="utf8mb4",
|
mysql_charset="utf8mb4",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class PermissionBits(Enum):
|
|
||||||
PermUser = 1
|
|
||||||
PermMod = 2
|
|
||||||
PermSysAdmin = 4
|
|
||||||
|
|
||||||
|
|
||||||
class UserData(BaseData):
|
class UserData(BaseData):
|
||||||
def create_user(
|
async def create_user(
|
||||||
self,
|
self,
|
||||||
id: int = None,
|
id: int = None,
|
||||||
username: str = None,
|
username: str = None,
|
||||||
@@ -60,20 +52,20 @@ class UserData(BaseData):
|
|||||||
username=username, email=email, password=password, permissions=permission
|
username=username, email=email, password=password, permissions=permission
|
||||||
)
|
)
|
||||||
|
|
||||||
result = self.execute(conflict)
|
result = await self.execute(conflict)
|
||||||
if result is None:
|
if result is None:
|
||||||
return None
|
return None
|
||||||
return result.lastrowid
|
return result.lastrowid
|
||||||
|
|
||||||
def get_user(self, user_id: int) -> Optional[Row]:
|
async def get_user(self, user_id: int) -> Optional[Row]:
|
||||||
sql = select(aime_user).where(aime_user.c.id == user_id)
|
sql = select(aime_user).where(aime_user.c.id == user_id)
|
||||||
result = self.execute(sql)
|
result = await self.execute(sql)
|
||||||
if result is None:
|
if result is None:
|
||||||
return False
|
return False
|
||||||
return result.fetchone()
|
return result.fetchone()
|
||||||
|
|
||||||
def check_password(self, user_id: int, passwd: bytes = None) -> bool:
|
async def check_password(self, user_id: int, passwd: bytes = None) -> bool:
|
||||||
usr = self.get_user(user_id)
|
usr = await self.get_user(user_id)
|
||||||
if usr is None:
|
if usr is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -85,39 +77,46 @@ class UserData(BaseData):
|
|||||||
|
|
||||||
return bcrypt.checkpw(passwd, usr["password"].encode())
|
return bcrypt.checkpw(passwd, usr["password"].encode())
|
||||||
|
|
||||||
def reset_autoincrement(self, ai_value: int) -> None:
|
async def delete_user(self, user_id: int) -> None:
|
||||||
# ALTER TABLE isn't in sqlalchemy so we do this the ugly way
|
|
||||||
sql = f"ALTER TABLE aime_user AUTO_INCREMENT={ai_value}"
|
|
||||||
self.execute(sql)
|
|
||||||
|
|
||||||
def delete_user(self, user_id: int) -> None:
|
|
||||||
sql = aime_user.delete(aime_user.c.id == user_id)
|
sql = aime_user.delete(aime_user.c.id == user_id)
|
||||||
|
|
||||||
result = self.execute(sql)
|
result = await self.execute(sql)
|
||||||
if result is None:
|
if result is None:
|
||||||
self.logger.error(f"Failed to delete user with id {user_id}")
|
self.logger.error(f"Failed to delete user with id {user_id}")
|
||||||
|
|
||||||
def get_unregistered_users(self) -> List[Row]:
|
async def get_unregistered_users(self) -> List[Row]:
|
||||||
"""
|
"""
|
||||||
Returns a list of users who have not registered with the webui. They may or may not have cards.
|
Returns a list of users who have not registered with the webui. They may or may not have cards.
|
||||||
"""
|
"""
|
||||||
sql = select(aime_user).where(aime_user.c.password == None)
|
sql = select(aime_user).where(aime_user.c.password == None)
|
||||||
|
|
||||||
result = self.execute(sql)
|
result = await self.execute(sql)
|
||||||
if result is None:
|
if result is None:
|
||||||
return None
|
return None
|
||||||
return result.fetchall()
|
return result.fetchall()
|
||||||
|
|
||||||
def find_user_by_email(self, email: str) -> Row:
|
async def find_user_by_email(self, email: str) -> Row:
|
||||||
sql = select(aime_user).where(aime_user.c.email == email)
|
sql = select(aime_user).where(aime_user.c.email == email)
|
||||||
result = self.execute(sql)
|
result = await self.execute(sql)
|
||||||
if result is None:
|
if result is None:
|
||||||
return False
|
return False
|
||||||
return result.fetchone()
|
return result.fetchone()
|
||||||
|
|
||||||
def find_user_by_username(self, username: str) -> List[Row]:
|
async def find_user_by_username(self, username: str) -> List[Row]:
|
||||||
sql = aime_user.select(aime_user.c.username.like(f"%{username}%"))
|
sql = aime_user.select(aime_user.c.username.like(f"%{username}%"))
|
||||||
result = self.execute(sql)
|
result = await self.execute(sql)
|
||||||
if result is None:
|
if result is None:
|
||||||
return False
|
return False
|
||||||
return result.fetchall()
|
return result.fetchall()
|
||||||
|
|
||||||
|
async def change_password(self, user_id: int, new_passwd: str) -> bool:
|
||||||
|
sql = aime_user.update(aime_user.c.id == user_id).values(password = new_passwd)
|
||||||
|
|
||||||
|
result = await self.execute(sql)
|
||||||
|
return result is not None
|
||||||
|
|
||||||
|
async def change_username(self, user_id: int, new_name: str) -> bool:
|
||||||
|
sql = aime_user.update(aime_user.c.id == user_id).values(username = new_name)
|
||||||
|
|
||||||
|
result = await self.execute(sql)
|
||||||
|
return result is not None
|
||||||
|
|||||||
2
core/data/schema/versions/SBZV_5_rollback.sql
Normal file
2
core/data/schema/versions/SBZV_5_rollback.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE diva_profile
|
||||||
|
DROP skn_eqp;
|
||||||
2
core/data/schema/versions/SBZV_6_upgrade.sql
Normal file
2
core/data/schema/versions/SBZV_6_upgrade.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE diva_profile
|
||||||
|
ADD skn_eqp INT NOT NULL DEFAULT 0;
|
||||||
12
core/data/schema/versions/SDBT_4_rollback.sql
Normal file
12
core/data/schema/versions/SDBT_4_rollback.sql
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
|
ALTER TABLE chuni_score_playlog
|
||||||
|
CHANGE COLUMN isClear isClear TINYINT(1) NULL DEFAULT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE chuni_score_best
|
||||||
|
CHANGE COLUMN isSuccess isSuccess TINYINT(1) NULL DEFAULT NULL ;
|
||||||
|
|
||||||
|
ALTER TABLE chuni_score_playlog
|
||||||
|
DROP COLUMN ticketId;
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
12
core/data/schema/versions/SDBT_5_upgrade.sql
Normal file
12
core/data/schema/versions/SDBT_5_upgrade.sql
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
|
ALTER TABLE chuni_score_playlog
|
||||||
|
CHANGE COLUMN isClear isClear TINYINT(6) NULL DEFAULT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE chuni_score_best
|
||||||
|
CHANGE COLUMN isSuccess isSuccess INT(11) NULL DEFAULT NULL ;
|
||||||
|
|
||||||
|
ALTER TABLE chuni_score_playlog
|
||||||
|
ADD COLUMN ticketId INT(11) NULL AFTER machineType;
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
22
core/data/schema/versions/SDDT_5_rollback.sql
Normal file
22
core/data/schema/versions/SDDT_5_rollback.sql
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
SET FOREIGN_KEY_CHECKS=0;
|
||||||
|
|
||||||
|
ALTER TABLE ongeki_user_event_point DROP COLUMN version;
|
||||||
|
ALTER TABLE ongeki_user_event_point DROP COLUMN `rank`;
|
||||||
|
ALTER TABLE ongeki_user_event_point DROP COLUMN `type`;
|
||||||
|
ALTER TABLE ongeki_user_event_point DROP COLUMN date;
|
||||||
|
|
||||||
|
ALTER TABLE ongeki_user_tech_event DROP COLUMN version;
|
||||||
|
|
||||||
|
ALTER TABLE ongeki_user_mission_point DROP COLUMN version;
|
||||||
|
|
||||||
|
ALTER TABLE ongeki_static_events DROP COLUMN endDate;
|
||||||
|
|
||||||
|
DROP TABLE ongeki_tech_event_ranking;
|
||||||
|
DROP TABLE ongeki_static_music_ranking_list;
|
||||||
|
DROP TABLE ongeki_static_rewards;
|
||||||
|
DROP TABLE ongeki_static_present_list;
|
||||||
|
DROP TABLE ongeki_static_tech_music;
|
||||||
|
DROP TABLE ongeki_static_client_testmode;
|
||||||
|
DROP TABLE ongeki_static_game_point;
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS=1;
|
||||||
98
core/data/schema/versions/SDDT_6_upgrade.sql
Normal file
98
core/data/schema/versions/SDDT_6_upgrade.sql
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
SET FOREIGN_KEY_CHECKS=0;
|
||||||
|
|
||||||
|
ALTER TABLE ongeki_user_event_point ADD COLUMN version INTEGER NOT NULL;
|
||||||
|
ALTER TABLE ongeki_user_event_point ADD COLUMN `rank` INTEGER;
|
||||||
|
ALTER TABLE ongeki_user_event_point ADD COLUMN `type` INTEGER NOT NULL;
|
||||||
|
ALTER TABLE ongeki_user_event_point ADD COLUMN date VARCHAR(25);
|
||||||
|
|
||||||
|
ALTER TABLE ongeki_user_tech_event ADD COLUMN version INTEGER NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE ongeki_user_mission_point ADD COLUMN version INTEGER NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE ongeki_static_events ADD COLUMN endDate TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||||
|
|
||||||
|
CREATE TABLE ongeki_tech_event_ranking (
|
||||||
|
id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
|
||||||
|
user INT NOT NULL,
|
||||||
|
version INT NOT NULL,
|
||||||
|
date VARCHAR(25),
|
||||||
|
eventId INT NOT NULL,
|
||||||
|
`rank` INT,
|
||||||
|
totalPlatinumScore INT NOT NULL,
|
||||||
|
totalTechScore INT NOT NULL,
|
||||||
|
UNIQUE KEY ongeki_tech_event_ranking_uk (user, eventId),
|
||||||
|
CONSTRAINT ongeki_tech_event_ranking_ibfk1 FOREIGN KEY (user) REFERENCES aime_user(id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE ongeki_static_music_ranking_list (
|
||||||
|
id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
|
||||||
|
version INT NOT NULL,
|
||||||
|
musicId INT NOT NULL,
|
||||||
|
point INT NOT NULL,
|
||||||
|
userName VARCHAR(255),
|
||||||
|
UNIQUE KEY ongeki_static_music_ranking_list_uk (version, musicId)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE ongeki_static_rewards (
|
||||||
|
id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
|
||||||
|
version INT NOT NULL,
|
||||||
|
rewardId INT NOT NULL,
|
||||||
|
rewardName VARCHAR(255) NOT NULL,
|
||||||
|
itemKind INT NOT NULL,
|
||||||
|
itemId INT NOT NULL,
|
||||||
|
UNIQUE KEY ongeki_tech_event_ranking_uk (version, rewardId)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE ongeki_static_present_list (
|
||||||
|
id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
|
||||||
|
version INT NOT NULL,
|
||||||
|
presentId INT NOT NULL,
|
||||||
|
presentName VARCHAR(255) NOT NULL,
|
||||||
|
rewardId INT NOT NULL,
|
||||||
|
stock INT NOT NULL,
|
||||||
|
message VARCHAR(255),
|
||||||
|
startDate VARCHAR(25) NOT NULL,
|
||||||
|
endDate VARCHAR(25) NOT NULL,
|
||||||
|
UNIQUE KEY ongeki_static_present_list_uk (version, presentId, rewardId)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE ongeki_static_tech_music (
|
||||||
|
id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
|
||||||
|
version INT NOT NULL,
|
||||||
|
eventId INT NOT NULL,
|
||||||
|
musicId INT NOT NULL,
|
||||||
|
level INT NOT NULL,
|
||||||
|
UNIQUE KEY ongeki_static_tech_music_uk (version, musicId, eventId)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE ongeki_static_client_testmode (
|
||||||
|
id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
|
||||||
|
regionId INT NOT NULL,
|
||||||
|
placeId INT NOT NULL,
|
||||||
|
clientId VARCHAR(11) NOT NULL,
|
||||||
|
updateDate TIMESTAMP NOT NULL,
|
||||||
|
isDelivery BOOLEAN NOT NULL,
|
||||||
|
groupId INT NOT NULL,
|
||||||
|
groupRole INT NOT NULL,
|
||||||
|
continueMode INT NOT NULL,
|
||||||
|
selectMusicTime INT NOT NULL,
|
||||||
|
advertiseVolume INT NOT NULL,
|
||||||
|
eventMode INT NOT NULL,
|
||||||
|
eventMusicNum INT NOT NULL,
|
||||||
|
patternGp INT NOT NULL,
|
||||||
|
limitGp INT NOT NULL,
|
||||||
|
maxLeverMovable INT NOT NULL,
|
||||||
|
minLeverMovable INT NOT NULL,
|
||||||
|
UNIQUE KEY ongeki_static_client_testmode_uk (clientId)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE ongeki_static_game_point (
|
||||||
|
id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
|
||||||
|
`type` INT NOT NULL,
|
||||||
|
cost INT NOT NULL,
|
||||||
|
startDate VARCHAR(25) NOT NULL DEFAULT "2000-01-01 05:00:00.0",
|
||||||
|
endDate VARCHAR(25) NOT NULL DEFAULT "2099-01-01 05:00:00.0",
|
||||||
|
UNIQUE KEY ongeki_static_game_point_uk (`type`)
|
||||||
|
);
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS=1;
|
||||||
10
core/data/schema/versions/SDEZ_7_rollback.sql
Normal file
10
core/data/schema/versions/SDEZ_7_rollback.sql
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
ALTER TABLE mai2_profile_detail
|
||||||
|
DROP COLUMN mapStock;
|
||||||
|
|
||||||
|
ALTER TABLE mai2_profile_extend
|
||||||
|
DROP COLUMN selectResultScoreViewType;
|
||||||
|
|
||||||
|
ALTER TABLE mai2_profile_option
|
||||||
|
DROP COLUMN outFrameType,
|
||||||
|
DROP COLUMN touchVolume,
|
||||||
|
DROP COLUMN breakSlideVolume;
|
||||||
10
core/data/schema/versions/SDEZ_8_upgrade.sql
Normal file
10
core/data/schema/versions/SDEZ_8_upgrade.sql
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
ALTER TABLE mai2_profile_detail
|
||||||
|
ADD mapStock INT NULL AFTER playCount;
|
||||||
|
|
||||||
|
ALTER TABLE mai2_profile_extend
|
||||||
|
ADD selectResultScoreViewType INT NULL AFTER selectResultDetails;
|
||||||
|
|
||||||
|
ALTER TABLE mai2_profile_option
|
||||||
|
ADD outFrameType INT NULL AFTER dispCenter,
|
||||||
|
ADD touchVolume INT NULL AFTER slideVolume,
|
||||||
|
ADD breakSlideVolume INT NULL AFTER slideVolume;
|
||||||
54
core/data/schema/versions/SDGT_1_rollback.sql
Normal file
54
core/data/schema/versions/SDGT_1_rollback.sql
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
SET FOREIGN_KEY_CHECKS=0;
|
||||||
|
|
||||||
|
-- WARNING: This script is NOT idempotent! MAKE A BACKUP BEFORE RUNNING THIS SCRIPT!
|
||||||
|
|
||||||
|
-- Drop UK idac_user_vs_info_uk
|
||||||
|
ALTER TABLE idac_user_vs_info
|
||||||
|
DROP FOREIGN KEY idac_user_vs_info_ibfk_1,
|
||||||
|
DROP INDEX idac_user_vs_info_uk;
|
||||||
|
|
||||||
|
-- Drop the new columns added to the original table
|
||||||
|
ALTER TABLE idac_user_vs_info
|
||||||
|
DROP COLUMN battle_mode,
|
||||||
|
DROP COLUMN invalid,
|
||||||
|
DROP COLUMN str,
|
||||||
|
DROP COLUMN str_now,
|
||||||
|
DROP COLUMN lose_now;
|
||||||
|
|
||||||
|
-- Add back the old columns to the original table
|
||||||
|
ALTER TABLE idac_user_vs_info
|
||||||
|
ADD COLUMN group_key VARCHAR(25),
|
||||||
|
ADD COLUMN win_flg INT,
|
||||||
|
ADD COLUMN style_car_id INT,
|
||||||
|
ADD COLUMN course_id INT,
|
||||||
|
ADD COLUMN course_day INT,
|
||||||
|
ADD COLUMN players_num INT,
|
||||||
|
ADD COLUMN winning INT,
|
||||||
|
ADD COLUMN advantage_1 INT,
|
||||||
|
ADD COLUMN advantage_2 INT,
|
||||||
|
ADD COLUMN advantage_3 INT,
|
||||||
|
ADD COLUMN advantage_4 INT,
|
||||||
|
ADD COLUMN select_course_id INT,
|
||||||
|
ADD COLUMN select_course_day INT,
|
||||||
|
ADD COLUMN select_course_random INT,
|
||||||
|
ADD COLUMN matching_success_sec INT,
|
||||||
|
ADD COLUMN boost_flag INT;
|
||||||
|
|
||||||
|
-- Delete the data from the original table where group_key is NULL
|
||||||
|
DELETE FROM idac_user_vs_info
|
||||||
|
WHERE group_key IS NULL;
|
||||||
|
|
||||||
|
-- Insert data back to the original table from idac_user_vs_course_info
|
||||||
|
INSERT INTO idac_user_vs_info (user, group_key, win_flg, style_car_id, course_id, course_day, players_num, winning, advantage_1, advantage_2, advantage_3, advantage_4, select_course_id, select_course_day, select_course_random, matching_success_sec, boost_flag, vs_history, break_count, break_penalty_flag)
|
||||||
|
SELECT user, CONCAT(FLOOR(RAND()*(99999999999999-10000000000000+1)+10000000000000), 'A69E01A8888'), 0, 0, course_id, 0, 0, vs_cnt, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
||||||
|
FROM idac_user_vs_course_info;
|
||||||
|
|
||||||
|
-- Add back the constraints and indexes to the original table
|
||||||
|
ALTER TABLE idac_user_vs_info
|
||||||
|
ADD CONSTRAINT idac_user_vs_info_ibfk_1 FOREIGN KEY (user) REFERENCES aime_user(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
ADD UNIQUE KEY idac_user_vs_info_uk (user, group_key);
|
||||||
|
|
||||||
|
-- Drop the new table idac_user_vs_course_info
|
||||||
|
DROP TABLE IF EXISTS idac_user_vs_course_info;
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS=1;
|
||||||
71
core/data/schema/versions/SDGT_2_upgrade.sql
Normal file
71
core/data/schema/versions/SDGT_2_upgrade.sql
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
SET FOREIGN_KEY_CHECKS=0;
|
||||||
|
|
||||||
|
-- WARNING: This script is NOT idempotent! MAKE A BACKUP BEFORE RUNNING THIS SCRIPT!
|
||||||
|
|
||||||
|
-- Create the new table idac_user_vs_course_info
|
||||||
|
CREATE TABLE idac_user_vs_course_info (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
user INT,
|
||||||
|
battle_mode INT,
|
||||||
|
course_id INT,
|
||||||
|
vs_cnt INT,
|
||||||
|
vs_win INT,
|
||||||
|
CONSTRAINT idac_user_vs_course_info_fk FOREIGN KEY (user) REFERENCES aime_user(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
UNIQUE KEY idac_user_vs_course_info_uk (user, battle_mode, course_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- Insert data from the original table to the new tables
|
||||||
|
INSERT INTO idac_user_vs_course_info (user, battle_mode, course_id, vs_cnt, vs_win)
|
||||||
|
SELECT user, 1 as battle_mode, course_id, COUNT(winning) as vs_cnt, SUM(win_flg) as vs_win
|
||||||
|
FROM idac_user_vs_info
|
||||||
|
GROUP BY user, course_id;
|
||||||
|
|
||||||
|
-- Drop UK idac_user_vs_info_uk
|
||||||
|
ALTER TABLE idac_user_vs_info
|
||||||
|
DROP FOREIGN KEY idac_user_vs_info_ibfk_1,
|
||||||
|
DROP INDEX idac_user_vs_info_uk;
|
||||||
|
|
||||||
|
-- Drop/Add the old columns from the original table
|
||||||
|
ALTER TABLE idac_user_vs_info
|
||||||
|
DROP COLUMN group_key,
|
||||||
|
DROP COLUMN win_flg,
|
||||||
|
DROP COLUMN style_car_id,
|
||||||
|
DROP COLUMN course_id,
|
||||||
|
DROP COLUMN course_day,
|
||||||
|
DROP COLUMN players_num,
|
||||||
|
DROP COLUMN winning,
|
||||||
|
DROP COLUMN advantage_1,
|
||||||
|
DROP COLUMN advantage_2,
|
||||||
|
DROP COLUMN advantage_3,
|
||||||
|
DROP COLUMN advantage_4,
|
||||||
|
DROP COLUMN select_course_id,
|
||||||
|
DROP COLUMN select_course_day,
|
||||||
|
DROP COLUMN select_course_random,
|
||||||
|
DROP COLUMN matching_success_sec,
|
||||||
|
DROP COLUMN boost_flag,
|
||||||
|
|
||||||
|
ADD COLUMN battle_mode TINYINT UNSIGNED DEFAULT 1 NOT NULL AFTER user,
|
||||||
|
ADD COLUMN invalid INT DEFAULT 0,
|
||||||
|
ADD COLUMN str INT DEFAULT 0,
|
||||||
|
ADD COLUMN str_now INT DEFAULT 0,
|
||||||
|
ADD COLUMN lose_now INT DEFAULT 0;
|
||||||
|
|
||||||
|
-- Create a temporary table to store the records you want to keep
|
||||||
|
CREATE TEMPORARY TABLE temp_table AS
|
||||||
|
SELECT MIN(id) AS min_id
|
||||||
|
FROM idac_user_vs_info
|
||||||
|
GROUP BY battle_mode, user;
|
||||||
|
|
||||||
|
-- Delete records from the original table based on the temporary table
|
||||||
|
DELETE FROM idac_user_vs_info
|
||||||
|
WHERE id NOT IN (SELECT min_id FROM temp_table);
|
||||||
|
|
||||||
|
-- Drop the temporary table
|
||||||
|
DROP TEMPORARY TABLE IF EXISTS temp_table;
|
||||||
|
|
||||||
|
-- Add UK idac_user_vs_info_uk
|
||||||
|
ALTER TABLE idac_user_vs_info
|
||||||
|
ADD CONSTRAINT idac_user_vs_info_ibfk_1 FOREIGN KEY (user) REFERENCES aime_user(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
ADD UNIQUE KEY idac_user_vs_info_uk (user, battle_mode);
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS=1;
|
||||||
1073
core/frontend.py
1073
core/frontend.py
File diff suppressed because it is too large
Load Diff
@@ -1,4 +0,0 @@
|
|||||||
{% extends "core/frontend/index.jinja" %}
|
|
||||||
{% block content %}
|
|
||||||
<h1>{{ arcade.name }}</h1>
|
|
||||||
{% endblock content %}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
{% extends "core/frontend/index.jinja" %}
|
|
||||||
{% block content %}
|
|
||||||
<div class="row justify-content-md-center form-signin">
|
|
||||||
<div class="col col-lg-4">
|
|
||||||
<form id="create" action="/gate/gate.create" method="post">
|
|
||||||
<h1 class="h3 mb-3 fw-normal">Sign-up</h1>
|
|
||||||
|
|
||||||
<div class="form-floating mb-3">
|
|
||||||
<input class="form-control" name="access-code" id="access-code" type="text"
|
|
||||||
placeholder="00000000000000000000" value={{ code }} maxlength="20" required>
|
|
||||||
<label for="access-code">Access Code*</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-floating mb-3">
|
|
||||||
<input type="text" class="form-control" name="username" id="username" placeholder="username">
|
|
||||||
<label for="username">Username*</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-floating mb-3">
|
|
||||||
<input type="email" class="form-control" name="email" id="email" placeholder="example@example.com">
|
|
||||||
<label for="email">E-Mail*</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-floating mb-3">
|
|
||||||
<input type="password" class="form-control" name="passwd" id="passwd" placeholder="Password">
|
|
||||||
<label for="passwd">Password*</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!--
|
|
||||||
<div class="form-check text-start my-3">
|
|
||||||
<input class="form-check-input" type="checkbox" value="remember-me" id="remember-me">
|
|
||||||
<label class="form-check-label" for="remember-me">
|
|
||||||
Remember me
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
-->
|
|
||||||
|
|
||||||
<div class="alert alert-warning" role="alert">
|
|
||||||
If you have not registered a card with this server, you cannot create a WebUI account!
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="btn btn-primary w-100 py-2" type="submit">Sign-up</button>
|
|
||||||
<!-- <p class="mt-5 mb-3 text-body-secondary">© 2023 ARTEMiS</p>-->
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock content %}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
{% extends "core/frontend/index.jinja" %}
|
|
||||||
{% block content %}
|
|
||||||
<style>
|
|
||||||
.form-signin input[type="text"] {
|
|
||||||
margin-bottom: -1px;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-signin input[type="password"] {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
border-top-left-radius: 0;
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<div class="row justify-content-md-center form-signin">
|
|
||||||
<div class="col col-lg-4">
|
|
||||||
<form id="login" action="/gate/gate.login" method="post">
|
|
||||||
<h1 class="h3 mb-3 fw-normal">Login</h1>
|
|
||||||
|
|
||||||
{% include "core/frontend/widgets/err_banner.jinja" %}
|
|
||||||
|
|
||||||
<div class="form-floating">
|
|
||||||
<input form="login" class="form-control" name="access-code" id="access-code" type="text"
|
|
||||||
placeholder="00000000000000000000" maxlength="20" required>
|
|
||||||
<label for="access-code">Access Code</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-floating">
|
|
||||||
<input form="login" type="password" class="form-control" name="passwd" id="passwd" placeholder="Password">
|
|
||||||
<label for="passwd">Password</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!--
|
|
||||||
<div class="form-check text-start my-3">
|
|
||||||
<input class="form-check-input" type="checkbox" value="remember-me" id="remember-me">
|
|
||||||
<label class="form-check-label" for="remember-me">
|
|
||||||
Remember me
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
-->
|
|
||||||
|
|
||||||
<div class="alert alert-info" role="alert">
|
|
||||||
To register for the WebUI, type in the <strong>access code</strong> of your card, as shown in a game, and leave the
|
|
||||||
password field blank.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="btn btn-primary w-100 py-2" type="submit">Sign in</button>
|
|
||||||
<!-- <p class="mt-5 mb-3 text-body-secondary">© 2023 ARTEMiS</p>-->
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock content %}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en" data-bs-theme="dark">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>{{ title }}</title>
|
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css" rel="stylesheet"
|
|
||||||
integrity="sha384-4bw+/aepP/YC94hEpVNVgiZdgIC5+VKNBQNGCHeKRQN+PtmoHDEXuppvnDJzQIu9" crossorigin="anonymous">
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
{% include "core/frontend/widgets/topbar.jinja" %}
|
|
||||||
<div class="container">
|
|
||||||
{% block content %}
|
|
||||||
<h1>{{ server_name }}</h1>
|
|
||||||
{% endblock content %}
|
|
||||||
</div>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/js/bootstrap.bundle.min.js"
|
|
||||||
integrity="sha384-HwwvtgBNo3bZJJLYd8oVXjrBZt8cqVSpeBNS5n7C8IVInixGAoxmnlMuBnhbgrkm"
|
|
||||||
crossorigin="anonymous"></script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{% extends "core/frontend/index.jinja" %}
|
|
||||||
{% block content %}
|
|
||||||
{% include "core/frontend/widgets/err_banner.jinja" %}
|
|
||||||
<h1>Machine Management</h1>
|
|
||||||
{% endblock content %}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
{% extends "core/frontend/index.jinja" %}
|
|
||||||
{% block content %}
|
|
||||||
<h1>System Management</h1>
|
|
||||||
|
|
||||||
<div class="row" id="rowForm">
|
|
||||||
{% if sesh.permissions >= 2 %}
|
|
||||||
<div class="col-sm-6" style="max-width: 25%;">
|
|
||||||
<form id="usrLookup" name="usrLookup" action="/sys/lookup.user" class="form-inline">
|
|
||||||
<h3>User Search</h3>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="usrEmail">Email address</label>
|
|
||||||
<input type="email" class="form-control" id="usrEmail" name="usrEmail" aria-describedby="emailHelp">
|
|
||||||
</div>
|
|
||||||
OR
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="usrName">Username</label>
|
|
||||||
<input type="text" class="form-control" id="usrName" name="usrName">
|
|
||||||
</div>
|
|
||||||
OR
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="usrId">User ID</label>
|
|
||||||
<input type="number" class="form-control" id="usrId" name="usrId">
|
|
||||||
</div>
|
|
||||||
<br />
|
|
||||||
<button type="submit" class="btn btn-primary">Search</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if sesh.permissions >= 4 %}
|
|
||||||
<div class="col-sm-6" style="max-width: 25%;">
|
|
||||||
<form id="arcadeLookup" name="arcadeLookup" action="/sys/lookup.arcade" class="form-inline" >
|
|
||||||
<h3>Arcade Search</h3>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="arcadeName">Arcade Name</label>
|
|
||||||
<input type="text" class="form-control" id="arcadeName" name="arcadeName">
|
|
||||||
</div>
|
|
||||||
OR
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="arcadeId">Arcade ID</label>
|
|
||||||
<input type="number" class="form-control" id="arcadeId" name="arcadeId">
|
|
||||||
</div>
|
|
||||||
OR
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="arcadeUser">Owner User ID</label>
|
|
||||||
<input type="number" class="form-control" id="arcadeUser" name="arcadeUser">
|
|
||||||
</div>
|
|
||||||
<br />
|
|
||||||
<button type="submit" class="btn btn-primary">Search</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-6" style="max-width: 25%;">
|
|
||||||
<form id="cabLookup" name="cabLookup" action="/sys/lookup.cab" class="form-inline" >
|
|
||||||
<h3>Machine Search</h3>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="cabSerial">Machine Serial</label>
|
|
||||||
<input type="text" class="form-control" id="cabSerial" name="cabSerial">
|
|
||||||
</div>
|
|
||||||
OR
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="cabId">Machine ID</label>
|
|
||||||
<input type="number" class="form-control" id="cabId" name="cabId">
|
|
||||||
</div>
|
|
||||||
OR
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="cabAcId">Arcade ID</label>
|
|
||||||
<input type="number" class="form-control" id="cabAcId" name="cabAcId">
|
|
||||||
</div>
|
|
||||||
<br />
|
|
||||||
<button type="submit" class="btn btn-primary">Search</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="row" id="rowResult" style="margin: 10px;">
|
|
||||||
{% if sesh.permissions >= 2 %}
|
|
||||||
<div id="userSearchResult" class="col-sm-6" style="max-width: 25%;">
|
|
||||||
{% for usr in usrlist %}
|
|
||||||
<pre><a href=/user/{{ usr.id }}>{{ usr.id }} | {{ usr.username }}</a></pre>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if sesh.permissions >= 4 %}
|
|
||||||
<div id="arcadeSearchResult" class="col-sm-6" style="max-width: 25%;">
|
|
||||||
{% for ac in aclist %}
|
|
||||||
<pre><a href=/arcade/{{ ac.id }}>{{ ac.id }} | {{ ac.name }}</a></pre>
|
|
||||||
{% endfor %}
|
|
||||||
</div
|
|
||||||
><div id="cabSearchResult" class="col-sm-6" style="max-width: 25%;">
|
|
||||||
{% for cab in cablist %}
|
|
||||||
<a href=/cab/{{ cab.id }}><pre>{{ cab.id }} | {{ cab.game if cab.game is defined else "ANY " }} | {{ cab.serial }}</pre></a>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="row" id="rowAdd">
|
|
||||||
|
|
||||||
</div>
|
|
||||||
{% endblock content %}
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
{% extends "core/frontend/index.jinja" %}
|
|
||||||
{% block content %}
|
|
||||||
<h1>{{ sesh["username"] }}'s Account</h1>
|
|
||||||
|
|
||||||
<div class="card mb-3">
|
|
||||||
<div class="card-body">
|
|
||||||
<h3 class="card-title">Cards</h3>
|
|
||||||
<!--<h4 class="card-subtitle mb-2 text-body-secondary">Card subtitle</h4>-->
|
|
||||||
<p class="card-text">All aime cards are listed here for the given user.</p>
|
|
||||||
<a href="#" data-bs-toggle="modal" data-bs-target="#card-add" class="btn btn-primary mb-3">Add Card</a>
|
|
||||||
{% if cards is defined and cards|length > 0 %}
|
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<table class="table table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col">#</th>
|
|
||||||
<th scope="col">Access Code</th>
|
|
||||||
<th scope="col">Status</th>
|
|
||||||
<th scope="col">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for c in cards %}
|
|
||||||
<tr class="align-middle">
|
|
||||||
<th scope="row">{{ c.index }}</th>
|
|
||||||
<td>{{ c.access_code }}</td>
|
|
||||||
<td>
|
|
||||||
{% if c.status == 'Active'%}
|
|
||||||
<span class="badge rounded-pill text-bg-success">Active</span>
|
|
||||||
{% elif c.status == 'Locked' %}
|
|
||||||
<span class="badge rounded-pill text-bg-warning">Locked</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if c.status == 'Active'%}
|
|
||||||
<button type="button" class="btn btn-warning btn-sm">Lock</i></button>
|
|
||||||
{% elif c.status == 'Locked' %}
|
|
||||||
<button type="button" class="btn btn-success btn-sm">Unlock</i></button>
|
|
||||||
{% endif %}
|
|
||||||
<button type="button" class="btn btn-danger btn-sm">Delete</i></button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="alert alert-warning" role="alert">
|
|
||||||
You have no cards registered to your account. Please add one.
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<!--<a href="#" data-bs-toggle="modal" data-bs-target="#card-add" class="card-link">Add Card</a>-->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body">
|
|
||||||
<h3 class="card-title">Arcades</h3>
|
|
||||||
<!--<h4 class="card-subtitle mb-2 text-body-secondary">Card subtitle</h4>-->
|
|
||||||
<p class="card-text">All arcades connected to the given account are listed here.</p>
|
|
||||||
{% if arcades is defined and arcades|length > 0 %}
|
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<table class="table table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col">#</th>
|
|
||||||
<th scope="col">Name</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for a in arcades %}
|
|
||||||
<tr class="align-middle clickable-row" data-href=/arcade/{{ a.id }}>
|
|
||||||
<th scope="row">{{ a.index }}</th>
|
|
||||||
<td>{{ a.name }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="alert alert-info" role="alert">
|
|
||||||
You have no arcades connected to your account.
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal fade" id="card-add" tabindex="-1" aria-labelledby="card-add-label" aria-hidden="true">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h1 class="modal-title fs-5" id="card-add-label">Add Card</h1>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<h3 class="fs-5">How to:</h3>
|
|
||||||
Scan your card on any networked game and press the "View Access Code" button (varies by game) and enter
|
|
||||||
the 20
|
|
||||||
digit code below.
|
|
||||||
<hr>
|
|
||||||
FOR AMUSE IC CARDS: DO NOT ENTER THE CODE SHOWN ON THE BACK OF THE CARD ITSELF OR IT WILL NOT WORK!
|
|
||||||
<form>
|
|
||||||
<div class="form-floating mt-3">
|
|
||||||
<label for="access-code" class="col-form-label">Access Code</label>
|
|
||||||
<input maxlength="20" type="text" required pattern="[0-9]{20}" class="form-control"
|
|
||||||
id="access-code">
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
||||||
<button type="button" class="btn btn-primary">Add</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock content %}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
{% if error > 0 %}
|
|
||||||
<div class="alert alert-danger" role="alert">
|
|
||||||
{% if error == 1 %}
|
|
||||||
Card not registered, or wrong password
|
|
||||||
{% elif error == 2 %}
|
|
||||||
Missing or malformed access code
|
|
||||||
{% elif error == 3 %}
|
|
||||||
Failed to create user
|
|
||||||
{% elif error == 4 %}
|
|
||||||
Arcade not found
|
|
||||||
{% elif error == 5 %}
|
|
||||||
Machine not found
|
|
||||||
{% else %}
|
|
||||||
An unknown error occoured
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
<nav class="p-3 mb-3 navbar navbar-expand-lg bg-body-tertiary" aria-label="Thirteenth navbar example">
|
|
||||||
<div class="container">
|
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarsExample11"
|
|
||||||
aria-controls="navbarsExample11" aria-expanded="false" aria-label="Toggle navigation">
|
|
||||||
<span class="navbar-toggler-icon"></span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="collapse navbar-collapse d-lg-flex" id="navbarsExample11">
|
|
||||||
<a class="navbar-brand col-lg-3 me-0" href= />ARTEMiS</a>
|
|
||||||
<ul class="navbar-nav col-lg-6 justify-content-lg-center nav-pills">
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link {% if active_page == '/' %}active{% endif %}" aria-current="page" href= />Home</a>
|
|
||||||
</li>
|
|
||||||
{% for game in game_list %}
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link {% if active_page == game.url %}active{% endif %}" href=/game/{{ game.url }}>{{ game.name }}</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
<!--
|
|
||||||
<li class="nav-item dropdown">
|
|
||||||
<a class="nav-link dropdown-toggle" href="#" data-bs-toggle="dropdown"
|
|
||||||
aria-expanded="false">Dropdown</a>
|
|
||||||
<ul class="dropdown-menu">
|
|
||||||
<li><a class="dropdown-item" href="#">Action</a></li>
|
|
||||||
<li><a class="dropdown-item" href="#">Another action</a></li>
|
|
||||||
<li><a class="dropdown-item" href="#">Something else here</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
-->
|
|
||||||
</ul>
|
|
||||||
<div class="d-lg-flex col-lg-3 justify-content-lg-end">
|
|
||||||
{% if sesh is defined and sesh["user_id"] > 0 %}
|
|
||||||
<div class="btn-group dropdown">
|
|
||||||
<a href="#" class="d-block link-body-emphasis text-decoration-none dropdown-toggle"
|
|
||||||
data-bs-toggle="dropdown" aria-expanded="false">
|
|
||||||
{{ sesh["username"] }}
|
|
||||||
</a>
|
|
||||||
<ul class="dropdown-menu text-small">
|
|
||||||
{% if sesh["permissions"] >= 2 %}
|
|
||||||
<li><a class="dropdown-item {% if active_page == 'sys' %}active{% endif %}" href="/sys">System</a></li>
|
|
||||||
{% endif %}
|
|
||||||
<li><a class="dropdown-item" href="#">Settings</a></li>
|
|
||||||
<li><a class="dropdown-item {% if active_page == 'user' %}active{% endif %}" href="/user">Account</a></li>
|
|
||||||
<li>
|
|
||||||
<hr class="dropdown-divider">
|
|
||||||
</li>
|
|
||||||
<li><a class="dropdown-item" href="#">Sign out</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<a href="/gate"><button class="btn {% if active_page == 'gate' %}btn-primary{% else %}btn-outline-primary{% endif %}">Login</button></a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
146
core/mucha.py
146
core/mucha.py
@@ -1,21 +1,23 @@
|
|||||||
from typing import Dict, Any, Optional, List
|
from typing import Dict, Any, Optional
|
||||||
import logging, coloredlogs
|
import logging, coloredlogs
|
||||||
from logging.handlers import TimedRotatingFileHandler
|
from logging.handlers import TimedRotatingFileHandler
|
||||||
from twisted.web import resource
|
from starlette.requests import Request
|
||||||
from twisted.web.http import Request
|
from starlette.responses import PlainTextResponse
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from Crypto.Cipher import Blowfish
|
from Crypto.Cipher import Blowfish
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
from core import CoreConfig
|
from .config import CoreConfig
|
||||||
from core.utils import Utils
|
from .utils import Utils
|
||||||
|
from .title import TitleServlet
|
||||||
|
from .data import Data
|
||||||
|
from .const import *
|
||||||
|
|
||||||
class MuchaServlet:
|
class MuchaServlet:
|
||||||
|
mucha_registry: Dict[str, Dict[str, str]] = {}
|
||||||
def __init__(self, cfg: CoreConfig, cfg_dir: str) -> None:
|
def __init__(self, cfg: CoreConfig, cfg_dir: str) -> None:
|
||||||
self.config = cfg
|
self.config = cfg
|
||||||
self.config_dir = cfg_dir
|
self.config_dir = cfg_dir
|
||||||
self.mucha_registry: List[str] = []
|
|
||||||
|
|
||||||
self.logger = logging.getLogger("mucha")
|
self.logger = logging.getLogger("mucha")
|
||||||
log_fmt_str = "[%(asctime)s] Mucha | %(levelname)s | %(message)s"
|
log_fmt_str = "[%(asctime)s] Mucha | %(levelname)s | %(message)s"
|
||||||
@@ -37,91 +39,147 @@ class MuchaServlet:
|
|||||||
self.logger.setLevel(cfg.mucha.loglevel)
|
self.logger.setLevel(cfg.mucha.loglevel)
|
||||||
coloredlogs.install(level=cfg.mucha.loglevel, logger=self.logger, fmt=log_fmt_str)
|
coloredlogs.install(level=cfg.mucha.loglevel, logger=self.logger, fmt=log_fmt_str)
|
||||||
|
|
||||||
all_titles = Utils.get_all_titles()
|
self.data = Data(cfg)
|
||||||
|
|
||||||
for _, mod in all_titles.items():
|
for _, mod in TitleServlet.title_registry.items():
|
||||||
if hasattr(mod, "index") and hasattr(mod.index, "get_mucha_info"):
|
enabled, game_cds, netids = mod.get_mucha_info(self.config, self.config_dir)
|
||||||
enabled, game_cd = mod.index.get_mucha_info(
|
if enabled:
|
||||||
self.config, self.config_dir
|
for x in range(len(game_cds)):
|
||||||
)
|
self.mucha_registry[game_cds[x]] = { "netid_prefix": netids[x] }
|
||||||
if enabled:
|
|
||||||
self.mucha_registry.append(game_cd)
|
|
||||||
|
|
||||||
self.logger.info(f"Serving {len(self.mucha_registry)} games")
|
self.logger.info(f"Serving {len(self.mucha_registry)} games")
|
||||||
|
|
||||||
def handle_boardauth(self, request: Request, _: Dict) -> bytes:
|
async def handle_boardauth(self, request: Request) -> bytes:
|
||||||
req_dict = self.mucha_preprocess(request.content.getvalue())
|
bod = await request.body()
|
||||||
|
req_dict = self.mucha_preprocess(bod)
|
||||||
client_ip = Utils.get_ip_addr(request)
|
client_ip = Utils.get_ip_addr(request)
|
||||||
|
|
||||||
if req_dict is None:
|
if req_dict is None:
|
||||||
self.logger.error(
|
self.logger.error(
|
||||||
f"Error processing mucha request {request.content.getvalue()}"
|
f"Error processing mucha request {bod}"
|
||||||
)
|
)
|
||||||
return b"RESULTS=000"
|
return PlainTextResponse("RESULTS=000")
|
||||||
|
|
||||||
req = MuchaAuthRequest(req_dict)
|
req = MuchaAuthRequest(req_dict)
|
||||||
self.logger.info(f"Boardauth request from {client_ip} for {req.gameVer}")
|
|
||||||
self.logger.debug(f"Mucha request {vars(req)}")
|
self.logger.debug(f"Mucha request {vars(req)}")
|
||||||
|
|
||||||
if req.gameCd not in self.mucha_registry:
|
if not req.gameCd or not req.gameVer or not req.sendDate or not req.countryCd or not req.serialNum:
|
||||||
self.logger.warning(f"Unknown gameCd {req.gameCd}")
|
self.logger.warn(f"Missing required fields - {vars(req)}")
|
||||||
return b"RESULTS=000"
|
return PlainTextResponse("RESULTS=000")
|
||||||
|
|
||||||
|
minfo = self.mucha_registry.get(req.gameCd, {})
|
||||||
|
|
||||||
|
if not minfo:
|
||||||
|
self.logger.warning(f"Unknown gameCd {req.gameCd} from {client_ip}")
|
||||||
|
return PlainTextResponse("RESULTS=000")
|
||||||
|
|
||||||
# TODO: Decrypt S/N
|
|
||||||
b_key = b""
|
b_key = b""
|
||||||
for x in range(8):
|
for x in range(8):
|
||||||
b_key += req.sendDate[(x - 1) & 7].encode()
|
b_key += req.sendDate[(x - 1) & 7].encode()
|
||||||
|
|
||||||
cipher = Blowfish.new(b_key, Blowfish.MODE_ECB)
|
b_iv = b_key # what the fuck namco
|
||||||
sn_decrypt = cipher.decrypt(bytes.fromhex(req.serialNum))
|
|
||||||
self.logger.debug(f"Decrypt SN to {sn_decrypt.hex()}")
|
cipher = Blowfish.new(b_key, Blowfish.MODE_CBC, b_iv)
|
||||||
|
try:
|
||||||
|
sn_decrypt = cipher.decrypt(bytes.fromhex(req.serialNum))[:12].decode()
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Decrypt SN {req.serialNum} failed! - {e}")
|
||||||
|
return PlainTextResponse("RESULTS=000")
|
||||||
|
|
||||||
|
self.logger.info(f"Boardauth request from {sn_decrypt} ({client_ip}) for {req.gameVer}")
|
||||||
|
|
||||||
resp = MuchaAuthResponse(
|
resp = MuchaAuthResponse(
|
||||||
f"{self.config.mucha.hostname}{':' + str(self.config.allnet.port) if self.config.server.is_develop else ''}"
|
f"{self.config.server.hostname}{':' + str(self.config.server.port) if not self.config.server.is_using_proxy else ''}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
netid = minfo.get('netid_prefix', "ABxN") + sn_decrypt[5:]
|
||||||
|
|
||||||
|
cab = await self.data.arcade.get_machine(netid)
|
||||||
|
if cab:
|
||||||
|
arcade = await self.data.arcade.get_arcade(cab['id'])
|
||||||
|
if not arcade:
|
||||||
|
self.logger.error(f"Failed to get arcade with id {cab['id']}")
|
||||||
|
return PlainTextResponse("RESULTS=000")
|
||||||
|
|
||||||
|
resp.AREA_0 = arcade["region_id"] or AllnetJapanRegionId.AICHI.name
|
||||||
|
resp.AREA_0_EN = arcade["region_id"] or AllnetJapanRegionId.AICHI.name
|
||||||
|
resp.AREA_FULL_0 = arcade["region_id"] or AllnetJapanRegionId.AICHI.name
|
||||||
|
resp.AREA_FULL_0_EN = arcade["region_id"] or AllnetJapanRegionId.AICHI.name
|
||||||
|
|
||||||
|
resp.AREA_1 = arcade["country"] or cab['country'] or AllnetCountryCode.JAPAN.value
|
||||||
|
resp.AREA_1_EN = arcade["country"] or cab['country'] or AllnetCountryCode.JAPAN.value
|
||||||
|
resp.AREA_FULL_1 = arcade["country"] or cab['country'] or AllnetCountryCode.JAPAN.value
|
||||||
|
resp.AREA_FULL_1_EN = arcade["country"] or cab['country'] or AllnetCountryCode.JAPAN.value
|
||||||
|
|
||||||
|
resp.AREA_2 = arcade["city"] if arcade["city"] else ""
|
||||||
|
resp.AREA_2_EN = arcade["city"] if arcade["city"] else ""
|
||||||
|
resp.AREA_FULL_2 = arcade["city"] if arcade["city"] else ""
|
||||||
|
resp.AREA_FULL_2_EN = arcade["city"] if arcade["city"] else ""
|
||||||
|
|
||||||
|
resp.AREA_3 = ""
|
||||||
|
resp.AREA_3_EN = ""
|
||||||
|
resp.AREA_FULL_3 = ""
|
||||||
|
resp.AREA_FULL_3_EN = ""
|
||||||
|
|
||||||
|
resp.PREFECTURE_ID = arcade['region_id']
|
||||||
|
resp.COUNTRY_CD = arcade['country'] or cab['country'] or AllnetCountryCode.JAPAN.value
|
||||||
|
resp.PLACE_ID = req.placeId if req.placeId else f"{arcade['country'] or cab['country'] or AllnetCountryCode.JAPAN.value}{arcade['id']:04X}"
|
||||||
|
resp.SHOP_NAME = arcade['name']
|
||||||
|
resp.SHOP_NAME_EN = arcade['name']
|
||||||
|
resp.SHOP_NICKNAME = arcade['nickname']
|
||||||
|
resp.SHOP_NICKNAME_EN = arcade['nickname']
|
||||||
|
|
||||||
|
elif self.config.server.allow_unregistered_serials:
|
||||||
|
self.logger.info(f"Allow unknown serial {netid} ({sn_decrypt}) to auth")
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.logger.warn(f'Auth failed for NetID {netid}')
|
||||||
|
return PlainTextResponse("RESULTS=000")
|
||||||
|
|
||||||
self.logger.debug(f"Mucha response {vars(resp)}")
|
self.logger.debug(f"Mucha response {vars(resp)}")
|
||||||
|
|
||||||
return self.mucha_postprocess(vars(resp))
|
return PlainTextResponse(self.mucha_postprocess(vars(resp)))
|
||||||
|
|
||||||
def handle_updatecheck(self, request: Request, _: Dict) -> bytes:
|
async def handle_updatecheck(self, request: Request) -> bytes:
|
||||||
req_dict = self.mucha_preprocess(request.content.getvalue())
|
bod = await request.body()
|
||||||
|
req_dict = self.mucha_preprocess(bod)
|
||||||
client_ip = Utils.get_ip_addr(request)
|
client_ip = Utils.get_ip_addr(request)
|
||||||
|
|
||||||
if req_dict is None:
|
if req_dict is None:
|
||||||
self.logger.error(
|
self.logger.error(
|
||||||
f"Error processing mucha request {request.content.getvalue()}"
|
f"Error processing mucha request {bod}"
|
||||||
)
|
)
|
||||||
return b"RESULTS=000"
|
return PlainTextResponse("RESULTS=000")
|
||||||
|
|
||||||
req = MuchaUpdateRequest(req_dict)
|
req = MuchaUpdateRequest(req_dict)
|
||||||
self.logger.info(f"Updatecheck request from {client_ip} for {req.gameVer}")
|
self.logger.info(f"Updatecheck request from {req.serialNum} ({client_ip}) for {req.gameVer}")
|
||||||
self.logger.debug(f"Mucha request {vars(req)}")
|
self.logger.debug(f"Mucha request {vars(req)}")
|
||||||
|
|
||||||
if req.gameCd not in self.mucha_registry:
|
if req.gameCd not in self.mucha_registry:
|
||||||
self.logger.warning(f"Unknown gameCd {req.gameCd}")
|
self.logger.warning(f"Unknown gameCd {req.gameCd}")
|
||||||
return b"RESULTS=000"
|
return PlainTextResponse("RESULTS=000")
|
||||||
|
|
||||||
resp = MuchaUpdateResponse(req.gameVer, f"{self.config.mucha.hostname}{':' + str(self.config.allnet.port) if self.config.server.is_develop else ''}")
|
resp = MuchaUpdateResponse(req.gameVer, f"{self.config.server.hostname}{':' + str(self.config.server.port) if not self.config.server.is_using_proxy else ''}")
|
||||||
|
|
||||||
self.logger.debug(f"Mucha response {vars(resp)}")
|
self.logger.debug(f"Mucha response {vars(resp)}")
|
||||||
|
|
||||||
return self.mucha_postprocess(vars(resp))
|
return PlainTextResponse(self.mucha_postprocess(vars(resp)))
|
||||||
|
|
||||||
def handle_dlstate(self, request: Request, _: Dict) -> bytes:
|
async def handle_dlstate(self, request: Request) -> bytes:
|
||||||
req_dict = self.mucha_preprocess(request.content.getvalue())
|
bod = await request.body()
|
||||||
|
req_dict = self.mucha_preprocess(bod)
|
||||||
client_ip = Utils.get_ip_addr(request)
|
client_ip = Utils.get_ip_addr(request)
|
||||||
|
|
||||||
if req_dict is None:
|
if req_dict is None:
|
||||||
self.logger.error(
|
self.logger.error(
|
||||||
f"Error processing mucha request {request.content.getvalue()}"
|
f"Error processing mucha request {bod}"
|
||||||
)
|
)
|
||||||
return b""
|
return PlainTextResponse("RESULTS=000")
|
||||||
|
|
||||||
req = MuchaDownloadStateRequest(req_dict)
|
req = MuchaDownloadStateRequest(req_dict)
|
||||||
self.logger.info(f"DownloadState request from {client_ip} for {req.gameCd} -> {req.updateVer}")
|
self.logger.info(f"DownloadState request from {req.serialNum} ({client_ip}) for {req.gameCd} -> {req.updateVer}")
|
||||||
self.logger.debug(f"request {vars(req)}")
|
self.logger.debug(f"request {vars(req)}")
|
||||||
return b"RESULTS=001"
|
return PlainTextResponse("RESULTS=001")
|
||||||
|
|
||||||
def mucha_preprocess(self, data: bytes) -> Optional[Dict]:
|
def mucha_preprocess(self, data: bytes) -> Optional[Dict]:
|
||||||
try:
|
try:
|
||||||
@@ -171,7 +229,7 @@ class MuchaAuthResponse:
|
|||||||
self.RESULTS = "001"
|
self.RESULTS = "001"
|
||||||
self.AUTH_INTERVAL = "86400"
|
self.AUTH_INTERVAL = "86400"
|
||||||
self.SERVER_TIME = datetime.strftime(datetime.now(), "%Y%m%d%H%M")
|
self.SERVER_TIME = datetime.strftime(datetime.now(), "%Y%m%d%H%M")
|
||||||
self.UTC_SERVER_TIME = datetime.strftime(datetime.now(pytz.UTC), "%Y%m%d%H%M")
|
self.SERVER_TIME_UTC = datetime.strftime(datetime.now(pytz.UTC), "%Y%m%d%H%M")
|
||||||
|
|
||||||
self.CHARGE_URL = f"https://{mucha_url}/charge/"
|
self.CHARGE_URL = f"https://{mucha_url}/charge/"
|
||||||
self.FILE_URL = f"https://{mucha_url}/file/"
|
self.FILE_URL = f"https://{mucha_url}/file/"
|
||||||
|
|||||||
19
core/templates/arcade/index.jinja
Normal file
19
core/templates/arcade/index.jinja
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{% extends "core/templates/index.jinja" %}
|
||||||
|
{% block content %}
|
||||||
|
{% if arcade is defined %}
|
||||||
|
<h1>{{ arcade.name }}</h1>
|
||||||
|
<h2>PCBs assigned to this arcade <button class="btn btn-success" id="btn_add_cab" onclick="toggle_add_cab_form()">Add</button></h2>
|
||||||
|
{% if success is defined and success == 3 %}
|
||||||
|
<div style="background-color: #00AA00; padding: 20px; margin-bottom: 10px; width: 15%;">
|
||||||
|
Cab added successfully
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<ul style="font-size: 20px;">
|
||||||
|
{% for c in arcade.cabs %}
|
||||||
|
<li><a href="/cab/{{ c.id }}">{{ c.serial }}</a> ({{ c.game if c.game else "Any" }}) <button class="btn btn-secondary" onclick="prep_edit_form()">Edit</button> <button class="btn-danger btn">Delete</button></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<h3>Arcade Not Found</h3>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock content %}
|
||||||
24
core/templates/gate/create.jinja
Normal file
24
core/templates/gate/create.jinja
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{% extends "core/templates/index.jinja" %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>Create User</h1>
|
||||||
|
<form id="create" style="max-width: 240px; min-width: 10%;" action="/gate/gate.create" method="post">
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="access_code">Card Access Code</label><br>
|
||||||
|
<input class="form-control" name="access_code" id="access_code" type="text" placeholder="00000000000000000000" value={{ code }} maxlength="20" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="username">Username</label><br>
|
||||||
|
<input id="username" class="form-control" name="username" type="text" placeholder="username">
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="email">Email</label><br>
|
||||||
|
<input id="email" class="form-control" name="email" type="email" placeholder="example@example.com">
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="passwd">Password</label><br>
|
||||||
|
<input id="passwd" class="form-control" name="passwd" type="password" placeholder="password">
|
||||||
|
</div>
|
||||||
|
<p></p>
|
||||||
|
<input id="submit" class="btn btn-primary" style="display: block; margin: 0 auto;" type="submit" value="Create">
|
||||||
|
</form>
|
||||||
|
{% endblock content %}
|
||||||
32
core/templates/gate/gate.jinja
Normal file
32
core/templates/gate/gate.jinja
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{% extends "core/templates/index.jinja" %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>Gate</h1>
|
||||||
|
{% include "core/templates/widgets/err_banner.jinja" %}
|
||||||
|
<style>
|
||||||
|
/* Chrome, Safari, Edge, Opera */
|
||||||
|
input::-webkit-outer-spin-button,
|
||||||
|
input::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Firefox */
|
||||||
|
input[type=number] {
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<form id="login" style="max-width: 240px; min-width: 10%;" action="/gate/gate.login" method="post">
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="access_code">Card Access Code</label><br>
|
||||||
|
<input form="login" class="form-control" name="access_code" id="access_code" type="number" placeholder="00000000000000000000" maxlength="20" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="passwd">Password</label><br>
|
||||||
|
<input id="passwd" class="form-control" name="passwd" type="password" placeholder="password">
|
||||||
|
</div>
|
||||||
|
<p></p>
|
||||||
|
<input id="submit" class="btn btn-primary" style="display: block; margin: 0 auto;" form="login" type="submit" value="Login">
|
||||||
|
</form>
|
||||||
|
<h6>*To register for the webui, type in the access code of your card, as shown in a game, and leave the password field blank.</h6>
|
||||||
|
<h6>*If you have not registered a card with this server, you cannot create a webui account.</h6>
|
||||||
|
{% endblock content %}
|
||||||
92
core/templates/index.jinja
Normal file
92
core/templates/index.jinja
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{{ title }}</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/jquery@3.2.1/dist/jquery.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script>
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
background-color: #181a1b !important;
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
color-scheme: dark !important;
|
||||||
|
}
|
||||||
|
html, body, input, textarea, select, button, dialog {
|
||||||
|
background-color: #181a1b;
|
||||||
|
}
|
||||||
|
html, body, input, textarea, select, button {
|
||||||
|
border-color: #736b5e;
|
||||||
|
color: #e8e6e3;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #3391ff;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
border-color: #545b5e;
|
||||||
|
}
|
||||||
|
::placeholder {
|
||||||
|
color: #b2aba1;
|
||||||
|
}
|
||||||
|
input:-webkit-autofill,
|
||||||
|
textarea:-webkit-autofill,
|
||||||
|
select:-webkit-autofill {
|
||||||
|
background-color: #404400 !important;
|
||||||
|
color: #e8e6e3 !important;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
background-color: #202324;
|
||||||
|
color: #aba499;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #454a4d;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: #575e62;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb:active {
|
||||||
|
background-color: #484e51;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-corner {
|
||||||
|
background-color: #181a1b;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
scrollbar-color: #454a4d #202324;
|
||||||
|
}
|
||||||
|
::selection {
|
||||||
|
background-color: #004daa !important;
|
||||||
|
color: #e8e6e3 !important;
|
||||||
|
}
|
||||||
|
::-moz-selection {
|
||||||
|
background-color: #004daa !important;
|
||||||
|
color: #e8e6e3 !important;
|
||||||
|
}
|
||||||
|
input[type="text"], input[type="text"]:focus, input[type="password"], input[type="password"]:focus, input[type="email"], input[type="email"]:focus {
|
||||||
|
background-color: #202324 !important;
|
||||||
|
color: #e8e6e3;
|
||||||
|
}
|
||||||
|
form {
|
||||||
|
outline: 1px solid grey;
|
||||||
|
padding: 20px;
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
.err-banner {
|
||||||
|
background-color: #AA0000;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
width: 15%;
|
||||||
|
}
|
||||||
|
.modal-content {
|
||||||
|
background-color: #181a1b;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% include "core/templates/widgets/topbar.jinja" %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>{{ server_name }}</h1>
|
||||||
|
{% endblock content %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
4
core/templates/machine/index.jinja
Normal file
4
core/templates/machine/index.jinja
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{% extends "core/templates/index.jinja" %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>Machine Management</h1>
|
||||||
|
{% endblock content %}
|
||||||
189
core/templates/sys/index.jinja
Normal file
189
core/templates/sys/index.jinja
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
{% extends "core/templates/index.jinja" %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>System Management</h1>
|
||||||
|
{% if error is defined %}
|
||||||
|
{% include "core/templates/widgets/err_banner.jinja" %}
|
||||||
|
{% endif %}
|
||||||
|
<h2>Search</h2>
|
||||||
|
<div class="row" id="rowForm">
|
||||||
|
{% if "{:08b}".format(sesh.permissions)[6] == "1" %}
|
||||||
|
<div class="col-sm-6" style="max-width: 25%;">
|
||||||
|
<form id="usrLookup" name="usrLookup" action="/sys/lookup.user" class="form-inline">
|
||||||
|
<h3>User Search</h3>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="usrId">User ID</label>
|
||||||
|
<input type="number" class="form-control" id="usrId" name="usrId">
|
||||||
|
</div>
|
||||||
|
OR
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="usrName">Username</label>
|
||||||
|
<input type="text" class="form-control" id="usrName" name="usrName">
|
||||||
|
</div>
|
||||||
|
OR
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="usrEmail">Email address</label>
|
||||||
|
<input type="email" class="form-control" id="usrEmail" name="usrEmail">
|
||||||
|
</div>
|
||||||
|
OR
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="usrAc">Access Code</label>
|
||||||
|
<input type="text" class="form-control" id="usrAc" name="usrAc" maxlength="20" placeholder="00000000000000000000">
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<button type="submit" class="btn btn-primary">Search</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if "{:08b}".format(sesh.permissions)[5] == "1" %}
|
||||||
|
<div class="col-sm-6" style="max-width: 25%;">
|
||||||
|
<form id="shopLookup" name="shopLookup" action="/sys/lookup.shop" class="form-inline">
|
||||||
|
<h3>Shop search</h3>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="shopId">Shop ID</label>
|
||||||
|
<input type="number" class="form-control" id="shopId" name="shopId">
|
||||||
|
</div>
|
||||||
|
OR
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="serialNum">Serial Number</label>
|
||||||
|
<input type="text" class="form-control" id="serialNum" name="serialNum" maxlength="15">
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<button type="submit" class="btn btn-primary">Search</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="row" id="rowResult" style="margin: 10px;">
|
||||||
|
{% if "{:08b}".format(sesh.permissions)[6] == "1" %}
|
||||||
|
<div id="userSearchResult" class="col-sm-6" style="max-width: 25%;">
|
||||||
|
{% for usr in usrlist %}
|
||||||
|
<a href=/user/{{ usr.id }}><pre>{{ usr.username if usr.username is not none else "<i>No Name Set</i>"}}</pre></a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if "{:08b}".format(sesh.permissions)[5] == "1" %}
|
||||||
|
<div id="shopSearchResult" class="col-sm-6" style="max-width: 25%;">
|
||||||
|
{% for shop in shoplist %}
|
||||||
|
<a href="/shop/{{ shop.id }}"><pre>{{ shop.name if shop.name else "<i>No Name Set</i>"}}</pre></a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<h2>Add</h2>
|
||||||
|
<div class="row" id="rowAdd">
|
||||||
|
{% if "{:08b}".format(sesh.permissions)[6] == "1" %}
|
||||||
|
<div class="col-sm-6" style="max-width: 25%;">
|
||||||
|
<form id="usrAdd" name="usrAdd" action="/sys/add.user" class="form-inline" method="POST">
|
||||||
|
<h3>Add User</h3>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="usrName">Username</label>
|
||||||
|
<input type="text" class="form-control" id="usrName" name="usrName">
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="usrEmail">Email address</label>
|
||||||
|
<input type="email" class="form-control" id="usrEmail" name="usrEmail" required>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="usrPerm">Permission Level</label>
|
||||||
|
<input type="number" class="form-control" id="usrPerm" name="usrPerm" value="1">
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<button type="submit" class="btn btn-primary">Add</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6" style="max-width: 25%;">
|
||||||
|
<form id="cardAdd" name="cardAdd" action="/sys/add.card" class="form-inline" method="POST">
|
||||||
|
<h3>Add Card</h3>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="cardUsr">User ID</label>
|
||||||
|
<input type="number" class="form-control" id="cardUsr" name="cardUsr" required>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="cardAc">Access Code</label>
|
||||||
|
<input type="text" class="form-control" id="cardAc" name="cardAc" maxlength="20" placeholder="00000000000000000000" required>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="cardIdm">IDm/Chip ID</label>
|
||||||
|
<input type="text" class="form-control" id="cardIdm" name="cardIdm" disabled>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<button type="submit" class="btn btn-primary">Add</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if "{:08b}".format(sesh.permissions)[5] == "1" %}
|
||||||
|
<div class="col-sm-6" style="max-width: 25%;">
|
||||||
|
<form id="shopAdd" name="shopAdd" action="/sys/add.shop" class="form-inline" method="POST">
|
||||||
|
<h3>Add Shop</h3>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="shopName">Name</label>
|
||||||
|
<input type="text" class="form-control" id="shopName" name="shopName">
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="shopCountry">Country Code</label>
|
||||||
|
<input type="text" class="form-control" id="shopCountry" name="shopCountry" maxlength="3" placeholder="JPN">
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="shopIp">VPN IP</label>
|
||||||
|
<input type="text" class="form-control" id="shopIp" name="shopIp">
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<button type="submit" class="btn btn-primary">Add</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6" style="max-width: 25%;">
|
||||||
|
<form id="cabAdd" name="cabAdd" action="/sys/add.cab" class="form-inline" method="POST">
|
||||||
|
<h3>Add Machine</h3>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="cabShop">Shop ID</label>
|
||||||
|
<input type="number" class="form-control" id="cabShop" name="cabShop" required>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="cabSerial">Serial</label>
|
||||||
|
<input type="text" class="form-control" id="cabSerial" name="cabSerial">
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="cabGame">Game Code</label>
|
||||||
|
<input type="text" class="form-control" id="cabGame" name="cabGame" maxlength="4" placeholder="SXXX">
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<button type="submit" class="btn btn-primary">Add</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="row" id="rowAddResult" style="margin: 10px;">
|
||||||
|
{% if "{:08b}".format(sesh.permissions)[6] == "1" %}
|
||||||
|
<div id="userAddResult" class="col-sm-6" style="max-width: 25%;">
|
||||||
|
{% if usradd is defined %}
|
||||||
|
<pre>Added user {{ usradd.username if usradd.username is not none else "with no name"}} with id {{usradd.id}} and password {{ usradd.password }}</pre>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div id="cardAddResult" class="col-sm-6" style="max-width: 25%;">
|
||||||
|
{% if cardadd is defined %}
|
||||||
|
<pre>Added {{ cardadd.access_code }} with id {{cardadd.id}} to user {{ cardadd.user }}</pre>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if "{:08b}".format(sesh.permissions)[5] == "1" %}
|
||||||
|
<div id="shopAddResult" class="col-sm-6" style="max-width: 25%;">
|
||||||
|
{% if shopadd is defined %}
|
||||||
|
<pre>Added Shop {{ shopadd.id }}</pre></a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div id="cabAddResult" class="col-sm-6" style="max-width: 25%;">
|
||||||
|
{% if cabadd is defined %}
|
||||||
|
<pre>Added Machine {{ cabadd.id }} with serial {{ cabadd.serial }}</pre></a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
||||||
175
core/templates/user/index.jinja
Normal file
175
core/templates/user/index.jinja
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
{% extends "core/templates/index.jinja" %}
|
||||||
|
{% block content %}
|
||||||
|
<script type="text/javascript">
|
||||||
|
function toggle_new_name_form() {
|
||||||
|
let frm = document.getElementById("new_name_form");
|
||||||
|
let btn = document.getElementById("btn_toggle_form");
|
||||||
|
|
||||||
|
if (frm.style['display'] != "") {
|
||||||
|
frm.style['display'] = "";
|
||||||
|
frm.style['max-height'] = "";
|
||||||
|
btn.innerText = "Cancel";
|
||||||
|
} else {
|
||||||
|
frm.style['display'] = "none";
|
||||||
|
frm.style['max-height'] = "0px";
|
||||||
|
btn.innerText = "Edit";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function toggle_add_card_form() {
|
||||||
|
let btn = document.getElementById("btn_add_card");
|
||||||
|
let dv = document.getElementById("add_card_container")
|
||||||
|
|
||||||
|
if (dv.style['display'] != "") {
|
||||||
|
btn.innerText = "Cancel";
|
||||||
|
dv.style['display'] = "";
|
||||||
|
} else {
|
||||||
|
btn.innerText = "Add";
|
||||||
|
dv.style['display'] = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function prep_edit_form(access_code, chip_id, idm, card_type, u_memo) {
|
||||||
|
ac = document.getElementById("card_edit_frm_access_code");
|
||||||
|
cid = document.getElementById("card_edit_frm_chip_id");
|
||||||
|
fidm = document.getElementById("card_edit_frm_idm");
|
||||||
|
memo = document.getElementById("card_edit_frm_memo");
|
||||||
|
|
||||||
|
if (chip_id == "None" || chip_id == undefined) {
|
||||||
|
chip_id = ""
|
||||||
|
}
|
||||||
|
if (idm == "None" || idm == undefined) {
|
||||||
|
idm = ""
|
||||||
|
}
|
||||||
|
if (u_memo == "None" || u_memo == undefined) {
|
||||||
|
u_memo = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
ac.value = access_code;
|
||||||
|
cid.value = chip_id;
|
||||||
|
fidm.value = idm;
|
||||||
|
memo.value = u_memo;
|
||||||
|
|
||||||
|
if (card_type == "AmusementIC") {
|
||||||
|
cid.disabled = true;
|
||||||
|
fidm.disabled = false;
|
||||||
|
} else {
|
||||||
|
cid.disabled = false;
|
||||||
|
fidm.disabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<h1>Management for {{ username }} <button onclick="toggle_new_name_form()" class="btn btn-secondary" id="btn_toggle_form">Edit</button></h1>
|
||||||
|
{% if error is defined %}
|
||||||
|
{% include "core/templates/widgets/err_banner.jinja" %}
|
||||||
|
{% endif %}
|
||||||
|
{% if success is defined and success == 2 %}
|
||||||
|
<div style="background-color: #00AA00; padding: 20px; margin-bottom: 10px; width: 15%;">
|
||||||
|
Update successful
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<form style="max-width: 33%; display: none; max-height: 0px;" action="/user/update.name" method="post" id="new_name_form">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="new_name" class="form-label">New Nickname</label>
|
||||||
|
<input type="text" class="form-control" id="new_name" name="new_name" aria-describedby="new_name_help">
|
||||||
|
<div id="new_name_help" class="form-text">Must be 10 characters or less</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Submit</button>
|
||||||
|
</form>
|
||||||
|
<p></p>
|
||||||
|
<h2>Cards <button class="btn btn-success" id="btn_add_card" onclick="toggle_add_card_form()">Add</button></h2>
|
||||||
|
{% if success is defined and success == 3 %}
|
||||||
|
<div style="background-color: #00AA00; padding: 20px; margin-bottom: 10px; width: 15%;">
|
||||||
|
Card added successfully
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div id="add_card_container" style="display: none; max-width: 33%;">
|
||||||
|
<form action="/user/add.card" method="post", id="frm_add_card">
|
||||||
|
<label class="form-label" for="card_add_frm_access_code">Access Code:</label>
|
||||||
|
<input class="form-control" name="add_access_code" id="card_add_frm_access_code" maxlength="20" type="text" required aria-describedby="ac_help">
|
||||||
|
<div id="ac_help" class="form-text">20 digit code on the back of the card.</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Add</button>
|
||||||
|
</form>
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
|
<ul style="font-size: 20px;">
|
||||||
|
{% for c in cards %}
|
||||||
|
<li>{{ c.access_code }} ({{ c.type}}): {{ c.status }} <button onclick="prep_edit_form('{{ c.access_code }}', '{{ c.chip_id}}', '{{ c.idm }}', '{{ c.type }}', '{{ c.memo }}')" data-bs-toggle="modal" data-bs-target="#card_edit" class="btn btn-secondary" id="btn_edit_card_{{ c.access_code }}">Edit</button> {% if c.status == 'Active'%}<button class="btn-warning btn">Lock</button>{% elif c.status == 'Locked' %}<button class="btn-warning btn">Unlock</button>{% endif %} <button class="btn-danger btn">Delete</button></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Reset Password</h2>
|
||||||
|
{% if success is defined and success == 1 %}
|
||||||
|
<div style="background-color: #00AA00; padding: 20px; margin-bottom: 10px; width: 15%;">
|
||||||
|
Update successful
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<form style="max-width: 33%;" action="/user/update.pw" method="post">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="current_pw" class="form-label">Current Password</label>
|
||||||
|
<input type="password" class="form-control" id="current_pw" name="current_pw">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="password1" class="form-label">New Password</label>
|
||||||
|
<input type="password" class="form-control" id="password1" name="password1" aria-describedby="password_help">
|
||||||
|
<div id="password_help" class="form-text">Password must be at least 10 characters long, contain an upper and lowercase character, number, and special character</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="password2" class="form-label">Retype New Password</label>
|
||||||
|
<input type="password" class="form-control" id="password2" name="password2">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Submit</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if arcades is defined and arcades|length > 0 %}
|
||||||
|
<h2>Arcades</h2>
|
||||||
|
<ul>
|
||||||
|
{% for a in arcades %}
|
||||||
|
<li><h3>{{ a.name }}</h3>
|
||||||
|
{% if a.machines|length > 0 %}
|
||||||
|
<table>
|
||||||
|
<tr><th>Serial</th><th>Game</th><th>Last Seen</th></tr>
|
||||||
|
{% for m in a.machines %}
|
||||||
|
<tr><td>{{ m.serial }}</td><td>{{ m.game }}</td><td>{{ m.last_seen }}</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="modal fade" id="card_edit" tabindex="-1" aria-labelledby="card_edit_label" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h1 class="modal-title fs-5" id="card_edit_label">Edit Card</h1>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form action="/user/edit.card" method="post" id="frm_edit_card">
|
||||||
|
<label class="form-label" for="card_edit_frm_access_code">Access Code:</label>
|
||||||
|
<input class="form-control" readonly name="add_access_code" id="card_edit_frm_access_code" maxlength="20" type="text" required aria-describedby="ac_help">
|
||||||
|
<div id="ac_help" class="form-text">20 digit code on the back of the card. If this is incorrect, contact a sysadmin.</div>
|
||||||
|
|
||||||
|
<label class="form-label" for="card_edit_frm_memo" id="card_edit_frm_memo_lbl">Memo:</label>
|
||||||
|
<input class="form-control" aria-describedby="memo_help" name="add_memo" id="card_edit_frm_memo" maxlength="16" type="text">
|
||||||
|
<div id="memo_help" class="form-text">Must be 16 characters or less.</div>
|
||||||
|
|
||||||
|
<label class="form-label" for="card_edit_frm_idm" id="card_edit_frm_idm_lbl">FeliCa IDm:</label>
|
||||||
|
<input class="form-control" aria-describedby="idm_help" name="add_felica_idm" id="card_edit_frm_idm" maxlength="16" type="text">
|
||||||
|
<div id="idm_help" class="form-text">8 bytes that uniquly idenfites a FeliCa card. Obtained by reading the card with an NFC reader.</div>
|
||||||
|
|
||||||
|
<label class="form-label" for="card_edit_frm_chip_id" id="card_edit_frm_chip_id_lbl">Mifare UID:</label>
|
||||||
|
<input class="form-control" aria-describedby="chip_id_help" name="add_mifare_chip_id" id="card_edit_frm_chip_id" maxlength="8" type="text">
|
||||||
|
<div id="chip_id_help" class="form-text">4 byte integer that uniquly identifies a Mifare card. Obtained by reading the card with an NFC reader.</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="submit" class="btn btn-primary" form="frm_edit_card">Edit</button>
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock content %}
|
||||||
28
core/templates/widgets/err_banner.jinja
Normal file
28
core/templates/widgets/err_banner.jinja
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{% if error > 0 %}
|
||||||
|
<div class="err-banner">
|
||||||
|
<h3>Error</h3>
|
||||||
|
{% if error == 1 %}
|
||||||
|
Card not registered, or wrong password
|
||||||
|
{% elif error == 2 %}
|
||||||
|
Missing or malformed access code
|
||||||
|
{% elif error == 3 %}
|
||||||
|
Failed to create user
|
||||||
|
{% elif error == 4 %}
|
||||||
|
Required field not filled or invalid
|
||||||
|
{% elif error == 5 %}
|
||||||
|
Incorrect old password
|
||||||
|
{% elif error == 6 %}
|
||||||
|
Passwords don't match
|
||||||
|
{% elif error == 7 %}
|
||||||
|
New password not acceptable
|
||||||
|
{% elif error == 8 %}
|
||||||
|
New Nickname too long
|
||||||
|
{% elif error == 9 %}
|
||||||
|
You must be logged in to preform this action
|
||||||
|
{% elif error == 10 %}
|
||||||
|
Invalid serial number
|
||||||
|
{% else %}
|
||||||
|
An unknown error occoured
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
22
core/templates/widgets/topbar.jinja
Normal file
22
core/templates/widgets/topbar.jinja
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<div style="background: #333; color: #f9f9f9; width: 10%; height: 50px; line-height: 50px; text-align: center; float: left;">
|
||||||
|
Navigation
|
||||||
|
</div>
|
||||||
|
<div style="background: #333; color: #f9f9f9; width: 80%; height: 50px; line-height: 50px; padding-left: 10px; float: left;">
|
||||||
|
<a href=/><button class="btn btn-primary">Home</button></a>
|
||||||
|
{% for game, data in game_list|items %}
|
||||||
|
<a href=/game{{ data.url }}/><button class="btn btn-success">{{ game }}</button></a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="background: #333; color: #f9f9f9; width: 10%; height: 50px; line-height: 50px; text-align: center; float: left;">
|
||||||
|
{% if sesh is defined and sesh["permissions"] >= 2 %}
|
||||||
|
<a href="/sys/"><button class="btn btn-primary">System</button></a>
|
||||||
|
{% endif %}
|
||||||
|
{% if sesh is defined and sesh["user_id"] > 0 %}
|
||||||
|
<a href="/user/"><button class="btn btn-primary">Account</button></a>
|
||||||
|
<a href="/user/logout"><button class="btn btn-danger">Logout</button></a>
|
||||||
|
{% else %}
|
||||||
|
<a href="/gate/"><button class="btn btn-primary">Gate</button></a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
150
core/title.py
150
core/title.py
@@ -1,20 +1,104 @@
|
|||||||
from typing import Dict, Any
|
from typing import Dict, List, Tuple, Any
|
||||||
|
import json
|
||||||
import logging, coloredlogs
|
import logging, coloredlogs
|
||||||
from logging.handlers import TimedRotatingFileHandler
|
from logging.handlers import TimedRotatingFileHandler
|
||||||
from twisted.web.http import Request
|
from starlette.requests import Request
|
||||||
|
from starlette.responses import Response
|
||||||
|
from starlette.routing import Route
|
||||||
|
|
||||||
from core.config import CoreConfig
|
from core.config import CoreConfig
|
||||||
from core.data import Data
|
from core.data import Data
|
||||||
from core.utils import Utils
|
from core.utils import Utils
|
||||||
|
|
||||||
|
class JSONResponseNoASCII(Response):
|
||||||
|
media_type = "application/json"
|
||||||
|
|
||||||
|
def render(self, content: Any) -> bytes:
|
||||||
|
return json.dumps(
|
||||||
|
content,
|
||||||
|
ensure_ascii=False,
|
||||||
|
).encode("utf-8")
|
||||||
|
|
||||||
|
class BaseServlet:
|
||||||
|
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
|
||||||
|
self.core_cfg = core_cfg
|
||||||
|
self.game_cfg = None
|
||||||
|
self.logger = logging.getLogger("title")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_game_enabled(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> bool:
|
||||||
|
"""Called during boot to check if a specific game code should load.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
game_code (str): 4 character game code
|
||||||
|
core_cfg (CoreConfig): CoreConfig class
|
||||||
|
cfg_dir (str): Config directory
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the game is enabled and set to run, False otherwise
|
||||||
|
|
||||||
|
"""
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_routes(self) -> List[Route]:
|
||||||
|
"""Called during boot to get all matcher endpoints this title servlet handles
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Route]: A list of Routes, WebSocketRoutes, or similar classes
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
Route("/{game}/{version}/{endpoint}", self.render_POST, methods=["POST"]),
|
||||||
|
Route("/{game}/{version}/{endpoint}", self.render_GET, methods=["GET"]),
|
||||||
|
]
|
||||||
|
|
||||||
|
def setup(self) -> None:
|
||||||
|
"""Called once during boot, should contain any additional setup the handler must do, such as starting any sub-services
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]:
|
||||||
|
"""Called any time a request to PowerOn is made to retrieve the url/host strings to be sent back to the game
|
||||||
|
|
||||||
|
Args:
|
||||||
|
game_code (str): 4 character game code
|
||||||
|
game_ver (int): version, expressed as an integer by multiplying by 100 (1.10 -> 110)
|
||||||
|
keychip (str): Keychip serial of the requesting machine, can be used to deliver specific URIs to different machines
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[str, str]: A tuple where offset 0 is the allnet uri field, and offset 1 is the allnet host field
|
||||||
|
"""
|
||||||
|
if not self.core_cfg.server.is_using_proxy and Utils.get_title_port(self.core_cfg) != 80:
|
||||||
|
return (f"http://{self.core_cfg.server.hostname}:{Utils.get_title_port(self.core_cfg)}/{game_code}/{game_ver}/", "")
|
||||||
|
|
||||||
|
return (f"http://{self.core_cfg.server.hostname}/{game_code}/{game_ver}/", "")
|
||||||
|
|
||||||
|
def get_mucha_info(self, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, List[str], List[str]]:
|
||||||
|
"""Called once during boot to check if this game is a mucha game
|
||||||
|
|
||||||
|
Args:
|
||||||
|
core_cfg (CoreConfig): CoreConfig class
|
||||||
|
cfg_dir (str): Config directory
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[bool, str]: Tuple where offset 0 is true if the game is enabled, false otherwise, and offset 1 is the game CD
|
||||||
|
"""
|
||||||
|
return (False, [], [])
|
||||||
|
|
||||||
|
async def render_POST(self, request: Request) -> bytes:
|
||||||
|
self.logger.warn(f"Game Does not dispatch POST")
|
||||||
|
return Response()
|
||||||
|
|
||||||
|
async def render_GET(self, request: Request) -> bytes:
|
||||||
|
self.logger.warn(f"Game Does not dispatch GET")
|
||||||
|
return Response()
|
||||||
|
|
||||||
class TitleServlet:
|
class TitleServlet:
|
||||||
|
title_registry: Dict[str, BaseServlet] = {}
|
||||||
def __init__(self, core_cfg: CoreConfig, cfg_folder: str):
|
def __init__(self, core_cfg: CoreConfig, cfg_folder: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.config = core_cfg
|
self.config = core_cfg
|
||||||
self.config_folder = cfg_folder
|
self.config_folder = cfg_folder
|
||||||
self.data = Data(core_cfg)
|
self.data = Data(core_cfg)
|
||||||
self.title_registry: Dict[str, Any] = {}
|
|
||||||
|
|
||||||
self.logger = logging.getLogger("title")
|
self.logger = logging.getLogger("title")
|
||||||
if not hasattr(self.logger, "initialized"):
|
if not hasattr(self.logger, "initialized"):
|
||||||
@@ -43,62 +127,62 @@ class TitleServlet:
|
|||||||
plugins = Utils.get_all_titles()
|
plugins = Utils.get_all_titles()
|
||||||
|
|
||||||
for folder, mod in plugins.items():
|
for folder, mod in plugins.items():
|
||||||
if hasattr(mod, "game_codes") and hasattr(mod, "index"):
|
if hasattr(mod, "game_codes") and hasattr(mod, "index") and hasattr(mod.index, "is_game_enabled"):
|
||||||
should_call_setup = True
|
should_call_setup = True
|
||||||
|
game_servlet: BaseServlet = mod.index
|
||||||
|
game_codes: List[str] = mod.game_codes
|
||||||
|
|
||||||
if hasattr(mod.index, "get_allnet_info"):
|
for code in game_codes:
|
||||||
for code in mod.game_codes:
|
if game_servlet.is_game_enabled(code, self.config, self.config_folder):
|
||||||
enabled, _, _ = mod.index.get_allnet_info(
|
handler_cls = game_servlet(self.config, self.config_folder)
|
||||||
code, self.config, self.config_folder
|
|
||||||
)
|
|
||||||
|
|
||||||
if enabled:
|
if hasattr(handler_cls, "setup") and should_call_setup:
|
||||||
handler_cls = mod.index(self.config, self.config_folder)
|
handler_cls.setup()
|
||||||
|
should_call_setup = False
|
||||||
|
|
||||||
if hasattr(handler_cls, "setup") and should_call_setup:
|
self.title_registry[code] = handler_cls
|
||||||
handler_cls.setup()
|
|
||||||
should_call_setup = False
|
|
||||||
|
|
||||||
self.title_registry[code] = handler_cls
|
|
||||||
|
|
||||||
else:
|
|
||||||
self.logger.warning(f"Game {folder} has no get_allnet_info")
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.logger.error(f"{folder} missing game_code or index in __init__.py")
|
self.logger.error(f"{folder} missing game_code or index in __init__.py, or is_game_enabled in index")
|
||||||
|
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
f"Serving {len(self.title_registry)} game codes {'on port ' + str(core_cfg.title.port) if core_cfg.title.port > 0 else ''}"
|
f"Serving {len(self.title_registry)} game codes {'on port ' + str(core_cfg.server.port) if core_cfg.server.port > 0 else ''}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def render_GET(self, request: Request, endpoints: dict) -> bytes:
|
def render_GET(self, request: Request, endpoints: dict) -> bytes:
|
||||||
code = endpoints["game"]
|
code = endpoints["title"]
|
||||||
|
subaction = endpoints['subaction']
|
||||||
|
|
||||||
if code not in self.title_registry:
|
if code not in self.title_registry:
|
||||||
self.logger.warning(f"Unknown game code {code}")
|
self.logger.warning(f"Unknown game code {code}")
|
||||||
request.setResponseCode(404)
|
request.setResponseCode(404)
|
||||||
return b""
|
return b""
|
||||||
|
|
||||||
index = self.title_registry[code]
|
index = self.title_registry[code]
|
||||||
if not hasattr(index, "render_GET"):
|
handler = getattr(index, f"{subaction}", None)
|
||||||
self.logger.warning(f"{code} does not dispatch GET")
|
if handler is None:
|
||||||
request.setResponseCode(405)
|
self.logger.error(f"{code} does not have handler for GET subaction {subaction}")
|
||||||
|
request.setResponseCode(500)
|
||||||
return b""
|
return b""
|
||||||
|
|
||||||
return index.render_GET(request, int(endpoints["version"]), endpoints["endpoint"])
|
return handler(request, code, endpoints)
|
||||||
|
|
||||||
def render_POST(self, request: Request, endpoints: dict) -> bytes:
|
def render_POST(self, request: Request, endpoints: dict) -> bytes:
|
||||||
code = endpoints["game"]
|
code = endpoints["title"]
|
||||||
|
subaction = endpoints['subaction']
|
||||||
|
|
||||||
if code not in self.title_registry:
|
if code not in self.title_registry:
|
||||||
self.logger.warning(f"Unknown game code {code}")
|
self.logger.warning(f"Unknown game code {code}")
|
||||||
request.setResponseCode(404)
|
request.setResponseCode(404)
|
||||||
return b""
|
return b""
|
||||||
|
|
||||||
index = self.title_registry[code]
|
index = self.title_registry[code]
|
||||||
if not hasattr(index, "render_POST"):
|
handler = getattr(index, f"{subaction}", None)
|
||||||
self.logger.warning(f"{code} does not dispatch POST")
|
if handler is None:
|
||||||
request.setResponseCode(405)
|
self.logger.error(f"{code} does not have handler for POST subaction {subaction}")
|
||||||
|
request.setResponseCode(500)
|
||||||
return b""
|
return b""
|
||||||
|
|
||||||
return index.render_POST(
|
endpoints.pop("title")
|
||||||
request, int(endpoints["version"]), endpoints["endpoint"]
|
endpoints.pop("subaction")
|
||||||
)
|
return handler(request, code, endpoints)
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
from typing import Dict, Any
|
from typing import Dict, Any, Optional
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from twisted.web.http import Request
|
from starlette.requests import Request
|
||||||
import logging
|
import logging
|
||||||
import importlib
|
import importlib
|
||||||
from os import walk
|
from os import walk
|
||||||
|
import jwt
|
||||||
|
from base64 import b64decode
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from .config import CoreConfig
|
||||||
|
|
||||||
class Utils:
|
class Utils:
|
||||||
|
real_title_port = None
|
||||||
|
real_title_port_ssl = None
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_all_titles(cls) -> Dict[str, ModuleType]:
|
def get_all_titles(cls) -> Dict[str, ModuleType]:
|
||||||
ret: Dict[str, Any] = {}
|
ret: Dict[str, Any] = {}
|
||||||
@@ -28,8 +34,53 @@ class Utils:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_ip_addr(cls, req: Request) -> str:
|
def get_ip_addr(cls, req: Request) -> str:
|
||||||
return (
|
ip = req.headers.get("x-forwarded-for", req.client.host)
|
||||||
req.getAllHeaders()[b"x-forwarded-for"].decode()
|
return ip.split(", ")[0]
|
||||||
if b"x-forwarded-for" in req.getAllHeaders()
|
|
||||||
else req.getClientAddress().host
|
@classmethod
|
||||||
)
|
def get_title_port(cls, cfg: CoreConfig):
|
||||||
|
if cls.real_title_port is not None: return cls.real_title_port
|
||||||
|
|
||||||
|
cls.real_title_port = cfg.server.proxy_port if cfg.server.is_using_proxy and cfg.server.proxy_port else cfg.server.port
|
||||||
|
|
||||||
|
return cls.real_title_port
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_title_port_ssl(cls, cfg: CoreConfig):
|
||||||
|
if cls.real_title_port_ssl is not None: return cls.real_title_port_ssl
|
||||||
|
|
||||||
|
cls.real_title_port_ssl = cfg.server.proxy_port_ssl if cfg.server.is_using_proxy and cfg.server.proxy_port_ssl else Utils.get_title_port(cfg)
|
||||||
|
|
||||||
|
return cls.real_title_port_ssl
|
||||||
|
|
||||||
|
def create_sega_auth_key(aime_id: int, game: str, place_id: int, keychip_id: str, b64_secret: str, exp_seconds: int = 86400, err_logger: str = 'aimedb') -> Optional[str]:
|
||||||
|
logger = logging.getLogger(err_logger)
|
||||||
|
try:
|
||||||
|
return jwt.encode({ "aime_id": aime_id, "game": game, "place_id": place_id, "keychip_id": keychip_id, "exp": int(datetime.now(tz=timezone.utc).timestamp()) + exp_seconds }, b64decode(b64_secret), algorithm="HS256")
|
||||||
|
except jwt.InvalidKeyError:
|
||||||
|
logger.error("Failed to encode Sega Auth Key because the secret is invalid!")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unknown exception occoured when encoding Sega Auth Key! {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def decode_sega_auth_key(token: str, b64_secret: str, err_logger: str = 'aimedb') -> Optional[Dict]:
|
||||||
|
logger = logging.getLogger(err_logger)
|
||||||
|
try:
|
||||||
|
return jwt.decode(token, "secret", b64decode(b64_secret), algorithms=["HS256"], options={"verify_signature": True})
|
||||||
|
except jwt.ExpiredSignatureError:
|
||||||
|
logger.error("Sega Auth Key failed to validate due to an expired signature!")
|
||||||
|
return None
|
||||||
|
except jwt.InvalidSignatureError:
|
||||||
|
logger.error("Sega Auth Key failed to validate due to an invalid signature!")
|
||||||
|
return None
|
||||||
|
except jwt.DecodeError as e:
|
||||||
|
logger.error(f"Sega Auth Key failed to decode! {e}")
|
||||||
|
return None
|
||||||
|
except jwt.InvalidTokenError as e:
|
||||||
|
logger.error(f"Sega Auth Key is invalid! {e}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unknown exception occoured when decoding Sega Auth Key! {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
75
dbutils.py
75
dbutils.py
@@ -1,9 +1,12 @@
|
|||||||
import yaml
|
#!/usr/bin/env python3
|
||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
from core.config import CoreConfig
|
from os import mkdir, path, access, W_OK
|
||||||
|
import yaml
|
||||||
|
import asyncio
|
||||||
|
|
||||||
from core.data import Data
|
from core.data import Data
|
||||||
from os import path, mkdir, access, W_OK
|
from core.config import CoreConfig
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(description="Database utilities")
|
parser = argparse.ArgumentParser(description="Database utilities")
|
||||||
@@ -16,19 +19,10 @@ if __name__ == "__main__":
|
|||||||
type=str,
|
type=str,
|
||||||
help="Version of the database to upgrade/rollback to",
|
help="Version of the database to upgrade/rollback to",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
|
||||||
"--game",
|
|
||||||
"-g",
|
|
||||||
type=str,
|
|
||||||
help="Game code of the game who's schema will be updated/rolled back. Ex. SDFE",
|
|
||||||
)
|
|
||||||
parser.add_argument("--email", "-e", type=str, help="Email for the new user")
|
parser.add_argument("--email", "-e", type=str, help="Email for the new user")
|
||||||
parser.add_argument("--old_ac", "-o", type=str, help="Access code to transfer from")
|
parser.add_argument("--access_code", "-a", type=str, help="Access code for new/transfer user", default="00000000000000000000")
|
||||||
parser.add_argument("--new_ac", "-n", type=str, help="Access code to transfer to")
|
parser.add_argument("--message", "-m", type=str, help="Revision message")
|
||||||
parser.add_argument("--force", "-f", type=bool, help="Force the action to happen")
|
parser.add_argument("action", type=str, help="create, upgrade, downgrade, create-owner, migrate, create-revision, create-autorevision")
|
||||||
parser.add_argument(
|
|
||||||
"action", type=str, help="DB Action, create, recreate, upgrade, or rollback"
|
|
||||||
)
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
cfg = CoreConfig()
|
cfg = CoreConfig()
|
||||||
@@ -51,41 +45,30 @@ if __name__ == "__main__":
|
|||||||
if args.action == "create":
|
if args.action == "create":
|
||||||
data.create_database()
|
data.create_database()
|
||||||
|
|
||||||
elif args.action == "recreate":
|
elif args.action == "upgrade":
|
||||||
data.recreate_database()
|
data.schema_upgrade(args.version)
|
||||||
|
|
||||||
elif args.action == "upgrade" or args.action == "rollback":
|
elif args.action == "downgrade":
|
||||||
if args.version is None:
|
if not args.version:
|
||||||
data.logger.warning("No version set, upgrading to latest")
|
logging.getLogger("database").error(f"Version argument required for downgrade")
|
||||||
|
exit(1)
|
||||||
if args.game is None:
|
data.schema_downgrade(args.version)
|
||||||
data.logger.warning("No game set, upgrading core schema")
|
|
||||||
data.migrate_database(
|
|
||||||
"CORE",
|
|
||||||
int(args.version) if args.version is not None else None,
|
|
||||||
args.action,
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
data.migrate_database(
|
|
||||||
args.game,
|
|
||||||
int(args.version) if args.version is not None else None,
|
|
||||||
args.action,
|
|
||||||
)
|
|
||||||
|
|
||||||
elif args.action == "autoupgrade":
|
|
||||||
data.autoupgrade()
|
|
||||||
|
|
||||||
elif args.action == "create-owner":
|
elif args.action == "create-owner":
|
||||||
data.create_owner(args.email)
|
loop = asyncio.get_event_loop()
|
||||||
|
loop.run_until_complete(data.create_owner(args.email, args.access_code))
|
||||||
|
|
||||||
elif args.action == "migrate-card":
|
elif args.action == "migrate":
|
||||||
data.migrate_card(args.old_ac, args.new_ac, args.force)
|
loop = asyncio.get_event_loop()
|
||||||
|
loop.run_until_complete(data.migrate())
|
||||||
|
|
||||||
elif args.action == "cleanup":
|
elif args.action == "create-revision":
|
||||||
data.delete_hanging_users()
|
loop = asyncio.get_event_loop()
|
||||||
|
loop.run_until_complete(data.create_revision(args.message))
|
||||||
|
|
||||||
elif args.action == "version":
|
elif args.action == "create-autorevision":
|
||||||
data.show_versions()
|
loop = asyncio.get_event_loop()
|
||||||
|
loop.run_until_complete(data.create_revision_auto(args.message))
|
||||||
|
|
||||||
data.logger.info("Done")
|
else:
|
||||||
|
logging.getLogger("database").info(f"Unknown action {args.action}")
|
||||||
|
|||||||
@@ -5,22 +5,23 @@ services:
|
|||||||
build: .
|
build: .
|
||||||
volumes:
|
volumes:
|
||||||
- ./aime:/app/aime
|
- ./aime:/app/aime
|
||||||
|
- ./configs/config:/app/config
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
CFG_DEV: 1
|
CFG_DEV: 1
|
||||||
CFG_CORE_SERVER_HOSTNAME: 0.0.0.0
|
CFG_CORE_SERVER_HOSTNAME: 0.0.0.0
|
||||||
CFG_CORE_DATABASE_HOST: ma.db
|
CFG_CORE_DATABASE_HOST: ma.db
|
||||||
CFG_CORE_MEMCACHED_HOSTNAME: ma.memcached
|
CFG_CORE_MEMCACHED_HOSTNAME: ma.memcached
|
||||||
CFG_CORE_AIMEDB_KEY: keyhere
|
CFG_CORE_AIMEDB_KEY: <INSERT AIMEDB KEY HERE>
|
||||||
CFG_CHUNI_SERVER_LOGLEVEL: debug
|
CFG_CHUNI_SERVER_LOGLEVEL: debug
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
- "8443:8443"
|
- "8443:8443"
|
||||||
- "22345:22345"
|
- "22345:22345"
|
||||||
|
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
- "8090:8090"
|
- "8090:8090"
|
||||||
|
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
@@ -28,21 +29,29 @@ services:
|
|||||||
|
|
||||||
db:
|
db:
|
||||||
hostname: ma.db
|
hostname: ma.db
|
||||||
image: mysql:8.0.31-debian
|
image: yobasystems/alpine-mariadb:10.11.5
|
||||||
environment:
|
environment:
|
||||||
MYSQL_DATABASE: aime
|
MYSQL_DATABASE: aime
|
||||||
MYSQL_USER: aime
|
MYSQL_USER: aime
|
||||||
MYSQL_PASSWORD: aime
|
MYSQL_PASSWORD: aime
|
||||||
MYSQL_ROOT_PASSWORD: AimeRootPassword
|
MYSQL_ROOT_PASSWORD: AimeRootPassword
|
||||||
|
MYSQL_CHARSET: utf8mb4
|
||||||
|
MYSQL_COLLATION: utf8mb4_general_ci
|
||||||
|
##Note: expose port 3306 to allow read.py importer into database, comment out when not needed
|
||||||
|
#ports:
|
||||||
|
# - "3306:3306"
|
||||||
|
##Note: uncomment to allow mysql to create a persistent database, leave commented if you want to rebuild database from scratch often
|
||||||
|
#volumes:
|
||||||
|
# - ./AimeDB:/var/lib/mysql
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
|
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost", "-pAimeRootPassword"]
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
|
|
||||||
memcached:
|
memcached:
|
||||||
hostname: ma.memcached
|
hostname: ma.memcached
|
||||||
image: memcached:1.6.17-bullseye
|
image: memcached:1.6.22-alpine3.18
|
||||||
|
command: [ "memcached", "-m", "1024", "-I", "128m" ]
|
||||||
|
|
||||||
phpmyadmin:
|
phpmyadmin:
|
||||||
hostname: ma.phpmyadmin
|
hostname: ma.phpmyadmin
|
||||||
@@ -53,5 +62,5 @@ services:
|
|||||||
PMA_PASSWORD: AimeRootPassword
|
PMA_PASSWORD: AimeRootPassword
|
||||||
APACHE_PORT: 8080
|
APACHE_PORT: 8080
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "9090:8080"
|
||||||
|
|
||||||
|
|||||||
246
docs/INSTALL_DOCKER.md
Normal file
246
docs/INSTALL_DOCKER.md
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
# ARTEMiS - Docker Installation Guide
|
||||||
|
|
||||||
|
This step-by-step guide will allow you to install a Contenerized Version of ARTEMiS inside Docker, some steps can be skipped assuming you already have pre-requisite components and modules installed.
|
||||||
|
|
||||||
|
This guide assumes using Debian 12(bookworm-stable) as a Host Operating System for most of packages and modules.
|
||||||
|
|
||||||
|
## Pre-Requisites:
|
||||||
|
|
||||||
|
- Linux-Based Operating System (e.g. Debian, Ubuntu)
|
||||||
|
- Docker (https://get.docker.com)
|
||||||
|
- Python 3.9+
|
||||||
|
- (optional) Git
|
||||||
|
|
||||||
|
## Install Python3.9+ and Docker
|
||||||
|
|
||||||
|
```
|
||||||
|
(if this is a fresh install of the system)
|
||||||
|
sudo apt update && sudo apt upgrade
|
||||||
|
|
||||||
|
(installs python3 and pip)
|
||||||
|
sudo apt install python3 python3-pip
|
||||||
|
|
||||||
|
(installs docker)
|
||||||
|
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||||
|
sh get-docker.sh
|
||||||
|
|
||||||
|
(optionally install git)
|
||||||
|
sudo apt install git
|
||||||
|
```
|
||||||
|
|
||||||
|
## Get ARTEMiS
|
||||||
|
|
||||||
|
If you installed git, clone into your choice of ARTEMiS git repository, e.g.:
|
||||||
|
```
|
||||||
|
git clone <ARTEMiS Repo> <folder>
|
||||||
|
```
|
||||||
|
If not, download the source package, and unpack it to the folder of your choice.
|
||||||
|
|
||||||
|
## Prepare development/home configuration
|
||||||
|
|
||||||
|
To build our Docker setup, first we need to create some folders and copy some files around
|
||||||
|
- Create 'aime', 'configs', 'AimeDB', and 'logs' folder in ARTEMiS root folder (where all source files exist)
|
||||||
|
- Inside configs folder, create 'config' folder, and copy all .yaml files from example_config to config (thats all files without nginx_example.conf)
|
||||||
|
- Edit .yaml files inside configs/config to suit your server needs
|
||||||
|
- Edit core.yaml inside configs/config:
|
||||||
|
```
|
||||||
|
set server.listen_address: to "0.0.0.0"
|
||||||
|
set title.hostname: to machine's IP address, e.g. "192.168.x.x", depending on your network, or actual hostname if your configuration is already set for dns resolve
|
||||||
|
set database.host: to "ma.db"
|
||||||
|
set database.memcached_host: to "ma.memcached"
|
||||||
|
set aimedb.key: to "<actual AIMEDB key>"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running Docker Compose
|
||||||
|
|
||||||
|
After configuring, go to ARTEMiS root folder, and execute:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
("-d" argument means detached or daemon, meaning you will regain control of your terminal and Containers will run in background)
|
||||||
|
|
||||||
|
This will start pulling and building required images from network, after it's done, a development server should be running, with server accessible under machine's IP, frontend with port 8090, and PHPMyAdmin under port 9090.
|
||||||
|
|
||||||
|
- To turn off the server, from ARTEMiS root folder, execute:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
- If you changed some files around, and don't see your changes applied, execute:
|
||||||
|
|
||||||
|
```
|
||||||
|
(turn off the server)
|
||||||
|
docker compose down
|
||||||
|
(rebuild)
|
||||||
|
docker compose build
|
||||||
|
(turn on)
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
- If you need to see logs from containers running, execute:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker compose logs
|
||||||
|
```
|
||||||
|
|
||||||
|
- add '-f' to the end if you want to follow logs.
|
||||||
|
|
||||||
|
## Running commands
|
||||||
|
|
||||||
|
If you need to execute python scripts supplied with the application, use `docker compose exec app python3 <script> <command>`, for example `docker compose exec app python3 dbutils.py version`
|
||||||
|
|
||||||
|
## Persistent DB
|
||||||
|
|
||||||
|
By default, in development mode, ARTEMiS database is stored temporarily, if you wish to keep your database saved between restarts, we need to bind the database inside the container to actual storage/folder inside our server, to do this we need to make a few changes:
|
||||||
|
|
||||||
|
- First off, edit docker-compose.yml, and uncomment 2 lines:
|
||||||
|
|
||||||
|
```
|
||||||
|
(uncomment these two)
|
||||||
|
#volumes:
|
||||||
|
# - ./AimeDB:/var/lib/mysql
|
||||||
|
```
|
||||||
|
|
||||||
|
- After that, start up the server, this time Database will be saved in AimeDB folder we created in our configuration steps.
|
||||||
|
- If you wish to save it in another folder and/or storage device, change the "./AimeDB" target folder to folder/device of your choice
|
||||||
|
|
||||||
|
NOTE (NEEDS FIX): at the moment running development mode with persistent DB will always run database creation script at the start of application, while it doesn't break database outright, it might create some issues, a temporary fix can be applied:
|
||||||
|
|
||||||
|
- Start up containers with persistent DB already enabled, let application create database
|
||||||
|
- After startup, `docker compose down` the instance
|
||||||
|
- Edit entrypoint.sh and remove the `python3 dbutils.py create` line from Development mode statement
|
||||||
|
- Execute `docker compose build` and `docker compose up -d` to rebuild the app and start the containers back
|
||||||
|
|
||||||
|
## Adding importer data
|
||||||
|
|
||||||
|
To add data using importer, we can do that a few ways:
|
||||||
|
|
||||||
|
### Use importer locally on server
|
||||||
|
|
||||||
|
For that we need actual GameData and Options supplied somehow to the server system, be it wsl2 mounting layer, a pendrive with data, network share, or a direct copy to the server storage
|
||||||
|
With python3 installed on system, install requirements.txt directly to the system, or through python3 virtual-environment (python3-venv)
|
||||||
|
Default mysql/mariadb client development packages will also be required
|
||||||
|
|
||||||
|
- In the system:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo apt install default-libmysqlclient-dev build-essential pkg-config libmemcached-dev
|
||||||
|
sudo apt install mysql-client
|
||||||
|
OR
|
||||||
|
sudo apt install libmariadb-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
- In the root ARTEMiS folder
|
||||||
|
|
||||||
|
```
|
||||||
|
python3 -m pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
- If we wish to layer that with python3 virtual-environment, install required system packages, then:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo apt install python3-venv
|
||||||
|
python3 -m venv /path/to/venv
|
||||||
|
cd /path/to/venv/bin
|
||||||
|
python3 -m pip install -r /path/to/artemis/requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
- Depending on how you installed, now you can run read.py using:
|
||||||
|
- For direct installation, from root ARTEMiS folder:
|
||||||
|
|
||||||
|
```
|
||||||
|
python3 read.py <args>
|
||||||
|
```
|
||||||
|
|
||||||
|
- Or from python3 virtual environment, from root ARTEMiS folder:
|
||||||
|
|
||||||
|
```
|
||||||
|
/path/to/python3-venv/bin/python3 /path/to/artemis/read.py <args>
|
||||||
|
```
|
||||||
|
|
||||||
|
- We need to expose database container port, so that read.py can communicate with the database, inside docker-compose.yml, uncomment 2 lines in the database container declaration (db):
|
||||||
|
|
||||||
|
```
|
||||||
|
#ports:
|
||||||
|
# - "3306:3306"
|
||||||
|
```
|
||||||
|
|
||||||
|
- Now, `docker compose down && docker compose build && docker compose up -d` to restart containers
|
||||||
|
|
||||||
|
Now to insert the data, by default, docker doesn't expose container hostnames to root system, when trying to run read.py against a container, it will Error that hostname is not available, to fix that, we can add database hostname by hand to /etc/hosts:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo <editor of your choice> /etc/hosts
|
||||||
|
add '127.0.0.1 ma.db' to the table
|
||||||
|
save and close
|
||||||
|
```
|
||||||
|
|
||||||
|
- You can remove the line in /etc/hosts and de-expose the database port after successful import (this assumes you're using Persistent DB, as restarting the container without it will clear imported data).
|
||||||
|
|
||||||
|
### Use importer on remote Linux system
|
||||||
|
|
||||||
|
Follow the system and python portion of the guide, installing required packages and python3 modules, Download the ARTEMiS source.
|
||||||
|
|
||||||
|
- Edit core.yaml and insert it into config catalog:
|
||||||
|
|
||||||
|
```
|
||||||
|
database:
|
||||||
|
host: "<hostname of target system>"
|
||||||
|
```
|
||||||
|
|
||||||
|
- Expose port 3306 from database docker container to system, and allow port 3306 through system firewall to expose port to the system from which you will be importing data. (Remember to close down the database ports after finishing!)
|
||||||
|
|
||||||
|
- Import data using read.py
|
||||||
|
|
||||||
|
### Use importer on remote Windows system
|
||||||
|
|
||||||
|
Follow the [windows](docs/INSTALL_WINDOWS.md) guide for installing python dependencies, download the ARTEMiS source.
|
||||||
|
|
||||||
|
- Edit core.yaml and insert it into config catalog:
|
||||||
|
|
||||||
|
```
|
||||||
|
database:
|
||||||
|
host: "<hostname of target system>"
|
||||||
|
```
|
||||||
|
|
||||||
|
- Expose port 3306 from database docker container to system, and allow port 3306 through system firewall to expose port to the system from which you will be importing data.
|
||||||
|
- For Windows, also allow port 3306 outside the system so that read.py can communicate with remote database. (Remember to close down the database ports after finishing!)
|
||||||
|
|
||||||
|
# Troubleshooting
|
||||||
|
|
||||||
|
## Game does not connect to ARTEMiS Allnet Server
|
||||||
|
Double check your core.yaml if all addresses are correct and ports are correctly set and/or opened.
|
||||||
|
|
||||||
|
## Game does not connect to Title Server
|
||||||
|
Title server hostname requires your actual system hostname, from which you set up the Containers, or it's IP address, you can get the IP by using command `ip a` which will list all interfaces, and one of them should be your system IP (typically under eth0).
|
||||||
|
|
||||||
|
## Unhandled command in AimeDB
|
||||||
|
Make sure you have a proper AimeDB Key added to configuration.
|
||||||
|
|
||||||
|
## Memcached Error in ARTEMiS application causes errors in loading data
|
||||||
|
Currently when running ARTEMiS from master branch, there is a small bug that causes app to always configure memcached service to 127.0.0.1, to fix that, locate cache.py file in core/data, and edit:
|
||||||
|
|
||||||
|
```
|
||||||
|
memcache = pylibmc.Client([hostname]), binary=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
to:
|
||||||
|
|
||||||
|
```
|
||||||
|
memcache = pylibmc.Client(["ma.memcached"], binary=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
And build the containers again.
|
||||||
|
This will fix errors loading data from server.
|
||||||
|
(This is fixed in development branch)
|
||||||
|
|
||||||
|
## read.py "Can't connect to local server through socket '/run/mysqld/mysqld.sock'"
|
||||||
|
|
||||||
|
sqlalchemy by default reads any ip based connection as socket, thus trying to connect locally, please use a hostname (such as ma.db as in guide, and do not localhost) to force it to use a network interface.
|
||||||
|
|
||||||
|
### TODO:
|
||||||
|
|
||||||
|
- Production environment
|
||||||
107
docs/INSTALL_LINUX.md
Normal file
107
docs/INSTALL_LINUX.md
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
# Installing ARTEMiS on Linux
|
||||||
|
This guide assumes a fresh install of Debian 12 or Rasperry Pi OS. If you're using a different distrubution, your package manager commands and package names may be different then what's listed below. Please check with your repository's package manager for package names.
|
||||||
|
|
||||||
|
## Install prerequisits
|
||||||
|
### Python
|
||||||
|
Some installs may come with python already installed. You can verify this by trying the following commands:
|
||||||
|
- `python --version`
|
||||||
|
- `python3 --version`
|
||||||
|
- `python3.<minor version> --version` where `<minor version>` is a python 3 release (eg 11, 10)
|
||||||
|
|
||||||
|
If your python version is at least 3.7, you can move to the next step
|
||||||
|
|
||||||
|
### Libraries and other software
|
||||||
|
ARTEMiS depends on mysql and memcached. As stated above, package names may vary by distrubution, but this is generally what you should expect to install.
|
||||||
|
#### Rasperry Pi OS
|
||||||
|
`sudo apt install git mariadb-server python3-pip memcached libmemcached-dev `
|
||||||
|
|
||||||
|
#### Debian 12
|
||||||
|
`sudo apt install git mariadb-server python3-pip memcached libmemcached-dev default-libmysqlclient-dev pkg-config`
|
||||||
|
|
||||||
|
### Optional: Install proxy
|
||||||
|
If you intend to use a proxy (recomended for public-facing production setups), we recomend nginx
|
||||||
|
`sudo apt install nginx`
|
||||||
|
|
||||||
|
## Database setup
|
||||||
|
### mysql_secure_installation
|
||||||
|
If you already have your database installed and configured, and are able to log in, skip down to the [Creating the database](#creating-the-database) section below. Otherwise, setup your newly installed database.
|
||||||
|
|
||||||
|
`sudo mysql_secure_installation`
|
||||||
|
|
||||||
|
Leave the root password blank, do not switch to unix socket, do reset the root password to something secure, and answer yes to the rest of the prompts. You can then log into your database with `sudo mysql`
|
||||||
|
|
||||||
|
### Creating the database
|
||||||
|
Once you're logged in, run the following commands, as root, to set up our database. Make sure you note down whatever you decide to make the password for the aime account, as you will need it to configure artemis.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE USER 'aime'@'localhost' IDENTIFIED BY '<password>';
|
||||||
|
CREATE DATABASE aime;
|
||||||
|
GRANT Alter,Create,Delete,Drop,Index,Insert,References,Select,Update ON aime.* TO 'aime'@'localhost';
|
||||||
|
quit
|
||||||
|
```
|
||||||
|
We have now set up our new user, `aime`, created a database called `aime` and given our user all the permissions it needs on every table of that database.
|
||||||
|
|
||||||
|
### Configure memcached
|
||||||
|
Under the file /etc/memcached.conf, please make sure the following parameters are set:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Start with a cap of 64 megs of memory. It's reasonable, and the daemon default
|
||||||
|
# Note that the daemon will grow to this size, but does not start out holding this much
|
||||||
|
# memory
|
||||||
|
|
||||||
|
-I 128m
|
||||||
|
-m 1024
|
||||||
|
```
|
||||||
|
|
||||||
|
** This is mandatory to avoid memcached overload caused by Crossbeats or by massive profiles
|
||||||
|
|
||||||
|
Restart memcached using: sudo systemctl restart memcached
|
||||||
|
|
||||||
|
## Getting ARTEMiS
|
||||||
|
### Clone from gitea
|
||||||
|
use `git clone https://gitea.tendokyu.moe/Hay1tsme/artemis.git` to pull down ARTEMiS into a folder called `artemis` created at wherever your current working directory is. `cd` into `artemis`.
|
||||||
|
|
||||||
|
### Optional: Create a venv
|
||||||
|
Python venvs are a way to install and manage packages on a per-project basis and are recomended on systems that will have multiple python scripts running on them to avoid dependancy issues. If this server will be running ARTEMiS and ONLY ARTEMiS, then it is possible to get away without creating one. If you do want to create one, you will have to install an additional package:
|
||||||
|
|
||||||
|
`sudo apt install python3-venv` (like above, package name may vary depending on distro and python version)
|
||||||
|
|
||||||
|
Now, simply run `python -m venv .venv` (may have to use python3 or python 3.11 instead of python) to create your virtual environment in the folder `.venv`. In order to install packages and run scripts in this environment, you have to 'activate' it by running `source .venv/bin/activate`. Your terminal should now have (venv) appended to it.
|
||||||
|
|
||||||
|
### Optional: Use the develop branch
|
||||||
|
By default, pulling down ARTEMiS from gitea will pull the `master` branch. This branch is updated less frequently, but is considered stable and ready for production use. If you'd rather have more updates, but a possibility for instability or bugs, you can switch to the develop branch by running `git checkout develop`. You can run `git checkout master` to switch back to stable.
|
||||||
|
|
||||||
|
## Install python libraries
|
||||||
|
Run `pip install -r requirements.txt` to install all of ARTEMiS' dependencies. If any installs fail, you may have missed a step in the [Install prerequisits](#install-prerequisits) section above. If you're absolutly sure you didn't, submit an issue on gitea.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
### Copy example configs
|
||||||
|
From the `artemis` directory, run `cp -r example_config config` to copy the example configuration files to a new folder called `config`. All of the config changes you make will be done in the `config` folder.
|
||||||
|
|
||||||
|
### Optional: Generate AimeDB and Frontend JWT Secrets
|
||||||
|
AimeDB and the frontend utalize JSON Web Tokens (JWT) for card authentication and session cookies respectivly. While generating a secret for AimeDB is optional, if you intend to run the frontend, a secret is required. You can generate a secret easily by running:
|
||||||
|
|
||||||
|
`openssl rand --base64 64`
|
||||||
|
|
||||||
|
With 64 being the number of bytes. You shouldn't need to go higher then 64, but you can if desired. **NOTE: When pasting secrets into the config file, make sure you remove any newlines!**
|
||||||
|
|
||||||
|
### Edit `core.yaml`
|
||||||
|
Before editing `core.yaml`, you should familiarize yourself with the name and function of each of the config options. You can find a full list in [config.md](config.md)
|
||||||
|
|
||||||
|
Open `core.yaml` in the `config` folder in your prefered text editor. The only configuration option that it is absolutly mandatory to change is `aimedb`->`key`. This key must be set for the server to start, and the key must be correct, otherwise you will not be able to process aimedb requests. The correct key is floating around online, and finding it is left as an excersie to the reader.
|
||||||
|
|
||||||
|
Another option that should be changed is `database`->`password` to be the password you set when you created your database user. You did write it down somewhere, right?
|
||||||
|
|
||||||
|
Since you are presumably not running the games on the same computer you're installing this server on, you're going to want to change `server`->`hostname` to be whatever hostname or IP address other PCs can reach this server by. Note that some games reject IPs and require hostnames, so setting a hostname is always recomended over an IP.
|
||||||
|
|
||||||
|
### Edit game configs
|
||||||
|
Every game has their own yaml file with settings that you may want to tweek. `InitialD Zero` and `Pokken` both have `hostname` fields in their config file that you should edit, and some games support encryption, if supplied with proper keys.
|
||||||
|
|
||||||
|
### A note about IDZ
|
||||||
|
InitialD Zero is currently the only game where it is required to specify encryption information (the AES key and at least one RSA key) for the game to start. These keys are, like the aimedb key, floating around online and will not be provided. If you don't have the keys, and don't plan on anybody connecting to your server playing InitialD Zero, it's best to set `enabled` to `False` in idz.yaml to disable the game.
|
||||||
|
|
||||||
|
## Create database tables
|
||||||
|
ARTEMiS uses alembic to manage datbase versioning. `dbutils.py` acts as a wrapper for alembic, and can execute some necessassary database functions. To create the database tables, run `python dbutils.py create`. Confirm that there are no errors, and you're good to go. If you intend to use the frontend, you may also want to run `python dbutils.py create-owner -a <your 20 digit access code here>` to create a superuser account to log in with.
|
||||||
|
|
||||||
|
## Run ARTEMiS
|
||||||
|
Once you have everything configured properly, simply run `python index.py` to start ARTEMiS. Verify that clients can connect to all services (allnet, billing, aimedb, and game servers) and setup is complete.
|
||||||
@@ -1,83 +1,77 @@
|
|||||||
# ARTEMiS - Windows 10/11 Guide
|
# Installing ARTEMiS on Windows
|
||||||
This step-by-step guide assumes that you are using a fresh install of Windows 10/11 without MySQL installed, some of the steps can be skipped if you already have an installation with MySQL 8.0 or even some of the modules already present on your environment
|
This guide assumes a fresh install of Windows 10. Please be aware that due to the lack of memcached and the general woes of running a server on Windows, this is only recommended for local setups or small hosting-for-the-homies type servers.
|
||||||
|
|
||||||
# Setup
|
## Install prerequisites
|
||||||
## Install Python Python 3.9 (recommended) or 3.10
|
### Python
|
||||||
1. Download Python 3.9 : [Link](https://www.python.org/ftp/python/3.9.13/python-3.9.13-amd64.exe)
|
- Python versions from 3.8 to 3.11 work with ARTEMiS. We recommend 3.11.
|
||||||
2. Install python-3.9.13-amd64.exe
|
- https://www.python.org/ftp/python/3.11.7/python-3.11.7-amd64.exe
|
||||||
1. Select Customize installation
|
- Install using whichever options best suit your environment, making sure that the Python executable is on path, such that you can open CMD, type `python --version` and see the version of Python you have installed.
|
||||||
2. Make sure that pip, tcl/tk, and the for all users are checked and hit Next
|
- If you already have a working version of Python installed, skip this step.
|
||||||
3. Make sure that you enable "Create shortcuts for installed applications" and "Add Python to environment variables" and hit Install
|
|
||||||
|
|
||||||
## Install MySQL 8.0
|
### MariaDB
|
||||||
1. Download MySQL 8.0 Server : [Link](https://cdn.mysql.com//Downloads/MySQLInstaller/mysql-installer-web-community-8.0.31.0.msi)
|
- It is always recommended to use MariaDB over MySQL because Oracle is a terrible company.
|
||||||
2. Install mysql-installer-web-community-8.0.31.0.msi
|
- While the latest release of v10 is recommended, as it is an LTS release, v11 should work fine.
|
||||||
1. Click on "Add ..." on the side
|
- https://ftp.osuosl.org/pub/mariadb//mariadb-10.11.6/winx64-packages/mariadb-10.11.6-winx64.msi
|
||||||
2. Click on the "+" next to MySQL Servers
|
- REMEMBER YOUR ROOT PASSWORD SO YOU CAN LOG IN IN FUTURE STEPS.
|
||||||
3. Make sure MySQL Server 8.0.29 - X64 is under the products to be installed.
|
|
||||||
4. Hit Next and Next once installed
|
### Git
|
||||||
5. Select the configuration type "Development Computer"
|
- While technically optional, it is strongly recommended to obtain ARTEMiS via git clone instead of just downloading it.
|
||||||
6. Hit Next
|
- https://git-scm.com/download/win
|
||||||
7. Select "Use Legacy Authentication Method (Retain MySQL 5.x compatibility)" and hit Next
|
- It is recommended to use Notepad++ as the default editor (if you have it installed), other than that, the default settings should be fine.
|
||||||
8. Enter a root password and then hit Next >
|
|
||||||
9. Leave everything under Windows Service as default and hit Next >
|
### Optional: GUI database viewer
|
||||||
10. Click on Execute and for it to finish and hit Next> and then Finish
|
- Having a GUI database editor is recommended but not required.
|
||||||
3. Open MySQL 8.0 Command Line Client and login as your root user
|
- MariaDB will try to install HeidiSQL, but we recommend DBeaver.
|
||||||
4. Type those commands to create your user and the database
|
- https://dbeaver.io/download/
|
||||||
|
|
||||||
|
## Obtain ARTEMiS
|
||||||
|
### Via git (recommended)
|
||||||
|
- `git clone https://gitea.tendokyu.moe/Hay1tsme/artemis.git` via cmd in whatever folder you want to install ARTEMiS.
|
||||||
|
- You can switch to the develop branch for latest changes via `git checkout develop`.
|
||||||
|
|
||||||
|
### Via http download
|
||||||
|
- Download [here](https://gitea.tendokyu.moe/Hay1tsme/artemis/archive/master.zip).
|
||||||
|
- Develop branch can be found [here](https://gitea.tendokyu.moe/Hay1tsme/artemis/archive/develop.zip).
|
||||||
|
- Extract the zip file somewhere.
|
||||||
|
|
||||||
|
## Database setup
|
||||||
|
- Log into your server as root, either via GUI (recommended) or CMD
|
||||||
|
- Create the `aime` user, replace `<password>` with a password you choose. Remember it!
|
||||||
```
|
```
|
||||||
CREATE USER 'aime'@'localhost' IDENTIFIED BY 'MyStrongPass.';
|
CREATE USER 'aime'@'localhost' IDENTIFIED BY '<password>';
|
||||||
CREATE DATABASE aime;
|
CREATE DATABASE aime;
|
||||||
GRANT Alter,Create,Delete,Drop,Index,Insert,References,Select,Update ON aime.* TO 'aime'@'localhost';
|
GRANT Alter,Create,Delete,Drop,Index,Insert,References,Select,Update ON aime.* TO 'aime'@'localhost';
|
||||||
FLUSH PRIVILEGES;
|
|
||||||
exit;
|
|
||||||
```
|
```
|
||||||
|
- If you create the database via a GUI, make sure you grant all the above permissions.
|
||||||
|
|
||||||
## Install Python modules
|
## Create a venv
|
||||||
1. Change your work path to the artemis-master folder using 'cd' and install the requirements:
|
- Python virtual environments are a good way to manage packages and make dealing with python and pip easier.
|
||||||
> pip install -r requirements.txt
|
- `python -m pip venv venv`
|
||||||
|
- `venv\Scripts\activate.bat` to activate the venv whenever you need to interact with ARTEMiS.
|
||||||
|
- All the rest of the steps assume your venv is activated.
|
||||||
|
|
||||||
## Copy/Rename the folder example_config to config
|
## Install pip modules
|
||||||
|
- `pip install -r requirements.txt`
|
||||||
|
|
||||||
## Adjust /config/core.yaml
|
## Setup configuration
|
||||||
|
- Create a new `config` folder and copy the files in `example_config` over.
|
||||||
|
- edit `core.yaml`
|
||||||
|
- Put the password you created for the aime user into the `database` section.
|
||||||
|
- Put in the aimedb key (YOU DO NOT GENERATE THIS KEY, FIND IT SOMEWHERE).
|
||||||
|
- Set your hostname to be whatever hostname or IP address games can reach your server at (many games reject localhost and 127.0.0.1).
|
||||||
|
- Optional: generate base64-encoded secrets for aimedb and frontend.
|
||||||
|
- See [config.md](docs/config.md) for a full list of options.
|
||||||
|
- edit `idz.yaml`
|
||||||
|
- If you don't plan on anyone using your server to play Initial D Zero, it is best to disable it to cut down on console spam on boot.
|
||||||
|
- Edit other game yamls
|
||||||
|
- Add keys, set hostnames, ports, etc. Specific settings will depend on the game. See [game_specific_info](docs/game_specific_info.md).
|
||||||
|
|
||||||
1. Make sure to change the server listen_address to be set to your local machine IP (ex.: 192.168.1.xxx)
|
## Create Database Tables
|
||||||
- In case you want to run this only locally, set the following values:
|
- `python dbutils.py create`
|
||||||
```
|
|
||||||
server:
|
|
||||||
listen_address: 0.0.0.0
|
|
||||||
title:
|
|
||||||
hostname: localhost
|
|
||||||
```
|
|
||||||
2. Adjust the proper MySQL information you created earlier
|
|
||||||
3. Add the AimeDB key at the bottom of the file
|
|
||||||
4. If the webui is needed, change the flag from False to True
|
|
||||||
|
|
||||||
## Create the database tables for ARTEMiS
|
## Firewall
|
||||||
> python dbutils.py create
|
- If you're planning on serving games not on your PC, open at least ports 80, 8443, and 22345 in windows firewall
|
||||||
|
- Also set `listen_address` to either your local IP to serve on your LAN, or `0.0.0.0` for all interfaces, to accept connections from other places.
|
||||||
|
|
||||||
## Firewall Adjustements
|
## Start ARTEMiS
|
||||||
Make sure the following ports are open both on your router and local Windows firewall in case you want to use this for public use (NOT recommended):
|
- `python index.py`
|
||||||
> Port 80 (TCP), 443 (TCP), 8443 (TCP), 22345 (TCP), 8080 (TCP), 8090 (TCP) **webui, 8444 (TCP) **mucha
|
|
||||||
|
|
||||||
## Running the ARTEMiS instance
|
|
||||||
> python index.py
|
|
||||||
|
|
||||||
# Troubleshooting
|
|
||||||
|
|
||||||
## Game does not connect to ARTEMiS Allnet server
|
|
||||||
1. Double-check your core.yaml, the listen_address is most likely either not binded to the proper IP or the port is not opened
|
|
||||||
|
|
||||||
## Game does not connect to Title Server
|
|
||||||
1. Verify that your core.yaml is setup properly for both the server listen_address and title hostname
|
|
||||||
2. Boot your game and verify that an AllNet response does show and if it does, attempt to open the URI that is shown under a browser such as Edge, Chrome & Firefox.
|
|
||||||
3. If a page is shown, the server is working properly and if it doesn't, double check your port forwarding and also that you have entered the proper local IP under the Title hostname in core.yaml.
|
|
||||||
|
|
||||||
## Unhandled command under AimeDB
|
|
||||||
1. Double check your AimeDB key under core.yaml, it is incorrect.
|
|
||||||
|
|
||||||
## AttributeError: module 'collections' has no attribute 'Hashable'
|
|
||||||
1. This means the pyYAML module is obsolete, simply rerun pip with the -U (force update) flag, as shown below.
|
|
||||||
- Change your work path to the artemis-master (or artemis-develop) folder using 'cd' and run the following commands:
|
|
||||||
```
|
|
||||||
pip install -r requirements.txt -U
|
|
||||||
```
|
|
||||||
|
|||||||
102
docs/INSTALL_WINDOWS_old.md
Normal file
102
docs/INSTALL_WINDOWS_old.md
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# ARTEMiS - Windows 10/11 Guide
|
||||||
|
This step-by-step guide assumes that you are using a fresh install of Windows 10/11 without MySQL installed, some of the steps can be skipped if you already have an installation with MySQL 8.0 or even some of the modules already present on your environment
|
||||||
|
|
||||||
|
# Setup
|
||||||
|
## Install Python Python 3.9 (recommended) or 3.10
|
||||||
|
1. Download Python 3.9 : [Link](https://www.python.org/ftp/python/3.9.13/python-3.9.13-amd64.exe)
|
||||||
|
2. Install python-3.9.13-amd64.exe
|
||||||
|
1. Select Customize installation
|
||||||
|
2. Make sure that pip, tcl/tk, and the for all users are checked and hit Next
|
||||||
|
3. Make sure that you enable "Create shortcuts for installed applications" and "Add Python to environment variables" and hit Install
|
||||||
|
|
||||||
|
## Install MySQL 8.0
|
||||||
|
1. Download MySQL 8.0 Server : [Link](https://dev.mysql.com/get/Downloads/MySQLInstaller/mysql-installer-community-8.0.34.0.msi)
|
||||||
|
2. Install mysql-installer-web-community-8.0.34.0.msi
|
||||||
|
1. Click on "Add ..." on the side
|
||||||
|
2. Click on the "+" next to MySQL Servers
|
||||||
|
3. Make sure MySQL Server 8.0.34 - X64 is under the products to be installed.
|
||||||
|
4. Hit Next and Next once installed
|
||||||
|
5. Select the configuration type "Development Computer"
|
||||||
|
6. Hit Next
|
||||||
|
7. Select "Use Legacy Authentication Method (Retain MySQL 5.x compatibility)" and hit Next
|
||||||
|
8. Enter a root password and then hit Next >
|
||||||
|
9. Leave everything under Windows Service as default and hit Next >
|
||||||
|
10. Click on Execute and for it to finish and hit Next> and then Finish
|
||||||
|
3. Open MySQL 8.0 Command Line Client and login as your root user
|
||||||
|
4. Change `<Enter Password Here>` to a new password for the user aime, type those commands to create your user and the database
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE USER 'aime'@'localhost' IDENTIFIED BY '<Enter Password Here>';
|
||||||
|
CREATE DATABASE aime;
|
||||||
|
GRANT Alter,Create,Delete,Drop,Index,Insert,References,Select,Update ON aime.* TO 'aime'@'localhost';
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
|
exit;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Install Python modules
|
||||||
|
1. Change your work path to the artemis-master folder using 'cd' and install the requirements:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Copy/Rename the folder `example_config` to `config`
|
||||||
|
|
||||||
|
## Adjust `config/core.yaml`
|
||||||
|
|
||||||
|
1. Make sure to change the server `hostname` to be set to your local machine IP (ex.: 192.168.xxx.xxx)
|
||||||
|
- In case you want to run this only locally, set the following values:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
server:
|
||||||
|
listen_address: 0.0.0.0
|
||||||
|
title:
|
||||||
|
hostname: 192.168.xxx.xxx
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Adjust the proper MySQL information you created earlier
|
||||||
|
```yaml
|
||||||
|
database:
|
||||||
|
host: "localhost"
|
||||||
|
username: "aime"
|
||||||
|
password: "<Enter Password Here>"
|
||||||
|
name: "aime"
|
||||||
|
```
|
||||||
|
3. Add the AimeDB key at the bottom of the file
|
||||||
|
4. If the webui is needed, change the flag from False to True
|
||||||
|
|
||||||
|
## Create the database tables for ARTEMiS
|
||||||
|
|
||||||
|
```shell
|
||||||
|
python dbutils.py create
|
||||||
|
```
|
||||||
|
|
||||||
|
## Firewall Adjustements
|
||||||
|
Make sure the following ports are open both on your router and local Windows firewall in case you want to use this for public use (NOT recommended):
|
||||||
|
> Port 80 (TCP), 443 (TCP), 8443 (TCP), 22345 (TCP), 8080 (TCP), 8090 (TCP) **webui, 8444 (TCP) **mucha
|
||||||
|
|
||||||
|
## Running the ARTEMiS instance
|
||||||
|
```shell
|
||||||
|
python index.py
|
||||||
|
```
|
||||||
|
|
||||||
|
# Troubleshooting
|
||||||
|
|
||||||
|
## Game does not connect to ARTEMiS Allnet server
|
||||||
|
1. Double-check your core.yaml, the listen_address is most likely either not binded to the proper IP or the port is not opened
|
||||||
|
|
||||||
|
## Game does not connect to Title Server
|
||||||
|
1. Verify that your core.yaml is setup properly for both the server listen_address and title hostname
|
||||||
|
2. Boot your game and verify that an AllNet response does show and if it does, attempt to open the URI that is shown under a browser such as Edge, Chrome & Firefox.
|
||||||
|
3. If a page is shown, the server is working properly and if it doesn't, double check your port forwarding and also that you have entered the proper local IP under the Title hostname in core.yaml.
|
||||||
|
|
||||||
|
## Unhandled command under AimeDB
|
||||||
|
1. Double check your AimeDB key under core.yaml, it is incorrect.
|
||||||
|
|
||||||
|
## AttributeError: module 'collections' has no attribute 'Hashable'
|
||||||
|
1. This means the pyYAML module is obsolete, simply rerun pip with the -U (force update) flag, as shown below.
|
||||||
|
- Change your work path to the artemis-master (or artemis-develop) folder using 'cd' and run the following commands:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
pip install -r requirements.txt -U
|
||||||
|
```
|
||||||
@@ -1,16 +1,24 @@
|
|||||||
# ARTEMiS Configuration
|
# ARTEMiS Configuration
|
||||||
## Server
|
## Server
|
||||||
- `listen_address`: IP Address or hostname that the server will listen for connections on. Set to 127.0.0.1 for local only, or 0.0.0.0 for all interfaces. Default `127.0.0.1`
|
- `listen_address`: IP Address or hostname that the server will listen for connections on. Set to 127.0.0.1 for local only, or 0.0.0.0 for all interfaces. Default `127.0.0.1`
|
||||||
|
- `hostname`: Hostname that gets sent to clients to tell them where to connect. Games must be able to connect to your server via the hostname or IP you spcify here. Note that most games will reject `localhost` or `127.0.0.1`. Default `localhost`
|
||||||
|
- `port`: Port that the server will listen for connections on. Default `80`
|
||||||
|
- `ssl_key`: Location of the ssl server key for the secure title server. Ignored if you don't use SSL. Default `cert/title.key`
|
||||||
|
- `ssl_cert`: Location of the ssl server certificate for the secure title server. Must not be a self-signed SSL. Ignored if you don't use SSL. Default `cert/title.pem`
|
||||||
- `allow_user_registration`: Allows users to register in-game via the AimeDB `register` function. Disable to be able to control who can use cards on your server. Default `True`
|
- `allow_user_registration`: Allows users to register in-game via the AimeDB `register` function. Disable to be able to control who can use cards on your server. Default `True`
|
||||||
- `allow_unregistered_serials`: Allows games that do not have registered keychips to connect and authenticate. Disable to restrict who can connect to your server. Recomended to disable for production setups. Default `True`
|
- `allow_unregistered_serials`: Allows games that do not have registered keychips to connect and authenticate. Disable to restrict who can connect to your server. Recomended to disable for production setups. Default `True`
|
||||||
- `name`: Name for the server, used by some games in their default MOTDs. Default `ARTEMiS`
|
- `name`: Name for the server, used by some games in their default MOTDs. Default `ARTEMiS`
|
||||||
- `is_develop`: Flags that the server is a development instance without a proxy standing in front of it. Setting to `False` tells the server not to listen for SSL, because the proxy should be handling all SSL-related things, among other things. Default `True`
|
- `is_develop`: Flags that the server is a development instance, and enables some useful development features. Disable for production setups. Default `True`.
|
||||||
- `threading`: Flags that `reactor.run` should be called via the `Thread` standard library. May provide a speed boost, but removes the ability to kill the server via `Ctrl + C`. Default: `False`
|
- `is_using_proxy`: Flags that you'll be using some other software, such as nginx, to proxy requests, and to send `proxy_port` or `proxy_port_ssl` to games instead of `port`. Default `False`
|
||||||
|
- `proxy_port`: Which port your front-facing proxy will be listening on. Ignored if `is_using_proxy` is `False` or if set to `0`. Default `0`
|
||||||
|
- `proxy_port`: Which port your front-facing proxy will be listening for ssl connections on. Ignored if `is_using_proxy` is `False` or if set to `0`. Default `0`
|
||||||
- `log_dir`: Directory to store logs. Server MUST have read and write permissions to this directory or you will have issues. Default `logs`
|
- `log_dir`: Directory to store logs. Server MUST have read and write permissions to this directory or you will have issues. Default `logs`
|
||||||
|
- `check_arcade_ip`: Checks IPs against the `arcade` table in the database, if one is defined. Default `False`
|
||||||
|
- `strict_ip_checking`: Rejects clients if there is no IP in the `arcade` table for the respective arcade. Default `False`
|
||||||
## Title
|
## Title
|
||||||
- `loglevel`: Logging level for the title server. Default `info`
|
- `loglevel`: Logging level for the title server. Default `info`
|
||||||
- `hostname`: Hostname that gets sent to clients to tell them where to connect. Games must be able to connect to your server via the hostname or IP you spcify here. Note that most games will reject `localhost` or `127.0.0.1`. Default `localhost`
|
- `reboot_start_time`: 24 hour JST time that clients will see as the start of maintenance period, ex `04:00`. Leave blank for no maintenance time. Default: `""`
|
||||||
- `port`: Port that the title server will listen for connections on. Set to 0 to use the Allnet handler to reduce the port footprint. Default `8080`
|
- `reboot_end_time`: 24 hour JST time that clients will see as the end of maintenance period, ex `05:00`. Leave blank for no maintenance time. Default: `""`
|
||||||
## Database
|
## Database
|
||||||
- `host`: Host of the database. Default `localhost`
|
- `host`: Host of the database. Default `localhost`
|
||||||
- `username`: Username of the account the server should connect to the database with. Default `aime`
|
- `username`: Username of the account the server should connect to the database with. Default `aime`
|
||||||
@@ -18,24 +26,32 @@
|
|||||||
- `name`: Name of the database the server should expect. Default `aime`
|
- `name`: Name of the database the server should expect. Default `aime`
|
||||||
- `port`: Port the database server is listening on. Default `3306`
|
- `port`: Port the database server is listening on. Default `3306`
|
||||||
- `protocol`: Protocol used in the connection string, e.i `mysql` would result in `mysql://...`. Default `mysql`
|
- `protocol`: Protocol used in the connection string, e.i `mysql` would result in `mysql://...`. Default `mysql`
|
||||||
- `sha2_password`: Weather or not the password in the connection string should be hashed via SHA2. Default `False`
|
- `sha2_password`: Whether or not the password in the connection string should be hashed via SHA2. Default `False`
|
||||||
- `loglevel`: Logging level for the database. Default `warn`
|
- `loglevel`: Logging level for the database. Default `info`
|
||||||
- `user_table_autoincrement_start`: What the `aime_user` table ID autoincrememnt should start with. Default `10000`
|
|
||||||
- `memcached_host`: Host of the memcached server. Default `localhost`
|
- `memcached_host`: Host of the memcached server. Default `localhost`
|
||||||
## Frontend
|
## Frontend
|
||||||
- `enable`: Weather or not the frontend should be enabled. Default `False`
|
- `enable`: Whether or not the frontend servlet should run. Frontend can still be run via `python -m uvicorn core.frontend:app` even if this is set to `False`. Default `False`
|
||||||
- `port`: Port the frontend should listen for connections on. Default `8090`
|
- `port`: Port the frontend should listen on. Default `8080`
|
||||||
- `loglevel`: Logging level for the frontend server. Default `info`
|
- `loglevel`: Logging level for the frontend server. Default `info`
|
||||||
|
- `secret`: Base64-encoded JWT secret for session cookies, generated by you. Default `""`
|
||||||
## Allnet
|
## Allnet
|
||||||
|
- `standalone`: Whether allnet should launch it's own servlet on it's own port, or be part of the main servlet on the default port. Disable if you either have something proxying `naominet.jp` requests to port 80, or have port 80 set in `server` -> `port`
|
||||||
|
- `port`: Port the allnet server should listen for connections on if it's running standalone. Games are hardcoded to ask for port `80` so only change if you have a proxy redirecting properly. Ignored if `standalone` is `False`. Default `80`
|
||||||
- `loglevel`: Logging level for the allnet server. Default `info`
|
- `loglevel`: Logging level for the allnet server. Default `info`
|
||||||
- `port`: Port the allnet server should listen for connections on. Games are hardcoded to ask for port `80` so only change if you have a proxy redirecting properly. Default `80`
|
|
||||||
- `allow_online_updates`: Allow allnet to distribute online updates via DownloadOrders. This system is currently non-functional, so leave it disabled. Default `False`
|
- `allow_online_updates`: Allow allnet to distribute online updates via DownloadOrders. This system is currently non-functional, so leave it disabled. Default `False`
|
||||||
|
- `update_cfg_folder`: Folder where delivery INI files will be checked for. Ignored if `allow_online_updates` is `False`. Default `""`
|
||||||
## Billing
|
## Billing
|
||||||
- `port`: Port the billing server should listen for connections on. Games are hardcoded to ask for port `8443` so only change if you have a proxy redirecting properly. Set to 0 to use the allnet handler to reduce the number of ports the server eats up. Default `8443`
|
- `standalone`: Whether the billing server should launch it's own servlet on it's own port, or be part of the main servlet on the default port. Setting this to `True` requires that you have `ssl_key` and `ssl_cert` set. Default `False`
|
||||||
- `ssl_key`: Location of the ssl server key for the billing server. Ignored if `port` is set to `0` or `is_develop` set to `False`. Default `cert/server.key`
|
- `loglevel`: Logging level for the billing server. Default `info`
|
||||||
- `ssl_cert`: Location of the ssl server certificate for the billing server. Must match the CA distributed to users or the billing server will not connect. Ignored if `port` is set to `0` or `is_develop` is set to `False`. Default `cert/server.pem`
|
- `port`: Port the billing server should listen for connections on. Games are hardcoded to ask for port `8443` so only change if you have a proxy redirecting properly. Ignored if `standalone` is `False`. Default `8443`
|
||||||
|
- `ssl_key`: Location of the ssl server key for the billing server. Ignored if `standalone` is `False`. Default `cert/server.key`
|
||||||
|
- `ssl_cert`: Location of the ssl server certificate for the billing server. Ignored if `standalone` is `False`. Must match the CA distributed to users or the billing server will not connect. Default `cert/server.pem`
|
||||||
- `signing_key`: Location of the RSA Private key used to sign billing requests. Must match the public key distributed to users or the billing server will not connect. Default `cert/billing.key`
|
- `signing_key`: Location of the RSA Private key used to sign billing requests. Must match the public key distributed to users or the billing server will not connect. Default `cert/billing.key`
|
||||||
## Aimedb
|
## Aimedb
|
||||||
|
- `enable`: Whether or not aimedb should run. Default `True`
|
||||||
|
- `listen_address`: IP Address or hostname that the aimedb server will listen for connections on. Leave this blank to use the listen address under `server`. Default `""`
|
||||||
- `loglevel`: Logging level for the aimedb server. Default `info`
|
- `loglevel`: Logging level for the aimedb server. Default `info`
|
||||||
- `port`: Port the aimedb server should listen for connections on. Games are hardcoded to ask for port `22345` so only change if you have a proxy redirecting properly. Default `22345`
|
- `port`: Port the aimedb server should listen for connections on. Games are hardcoded to ask for port `22345` so only change if you have a proxy redirecting properly. Default `22345`
|
||||||
- `key`: Key to encrypt/decrypt aimedb requests and responses. MUST be set or the server will not start. If set incorrectly, your server will not properly handle aimedb requests. Default `""`
|
- `key`: Key to encrypt/decrypt aimedb requests and responses. MUST be set or the server will not start. If set incorrectly, your server will not properly handle aimedb requests. Default `""`
|
||||||
|
- `id_secret`: Base64-encoded JWT secret for Sega Auth IDs. Leaving this blank disables this feature. Default `""`
|
||||||
|
- `id_lifetime_seconds`: Number of secons a JWT generated should be valid for. Default `86400` (1 day)
|
||||||
|
|||||||
@@ -6,6 +6,12 @@ the corresponding importer and database upgrades.
|
|||||||
**Important: The described database upgrades are only required if you are using an old database schema, f.e. still
|
**Important: The described database upgrades are only required if you are using an old database schema, f.e. still
|
||||||
using the megaime database. Clean installations always create the latest database structure!**
|
using the megaime database. Clean installations always create the latest database structure!**
|
||||||
|
|
||||||
|
To upgrade the core database and the database for every game, execute:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
python dbutils.py autoupgrade
|
||||||
|
```
|
||||||
|
|
||||||
# Table of content
|
# Table of content
|
||||||
|
|
||||||
- [Supported Games](#supported-games)
|
- [Supported Games](#supported-games)
|
||||||
@@ -16,6 +22,7 @@ using the megaime database. Clean installations always create the latest databas
|
|||||||
- [Card Maker](#card-maker)
|
- [Card Maker](#card-maker)
|
||||||
- [WACCA](#wacca)
|
- [WACCA](#wacca)
|
||||||
- [Sword Art Online Arcade](#sao)
|
- [Sword Art Online Arcade](#sao)
|
||||||
|
- [Initial D THE ARCADE](#initial-d-the-arcade)
|
||||||
|
|
||||||
|
|
||||||
# Supported Games
|
# Supported Games
|
||||||
@@ -27,7 +34,7 @@ Games listed below have been tested and confirmed working.
|
|||||||
### SDBT
|
### SDBT
|
||||||
|
|
||||||
| Version ID | Version Name |
|
| Version ID | Version Name |
|
||||||
|------------|-----------------------|
|
| ---------- | --------------------- |
|
||||||
| 0 | CHUNITHM |
|
| 0 | CHUNITHM |
|
||||||
| 1 | CHUNITHM PLUS |
|
| 1 | CHUNITHM PLUS |
|
||||||
| 2 | CHUNITHM AIR |
|
| 2 | CHUNITHM AIR |
|
||||||
@@ -43,10 +50,11 @@ Games listed below have been tested and confirmed working.
|
|||||||
### SDHD/SDBT
|
### SDHD/SDBT
|
||||||
|
|
||||||
| Version ID | Version Name |
|
| Version ID | Version Name |
|
||||||
|------------|---------------------|
|
| ---------- | ------------------- |
|
||||||
| 11 | CHUNITHM NEW!! |
|
| 11 | CHUNITHM NEW!! |
|
||||||
| 12 | CHUNITHM NEW PLUS!! |
|
| 12 | CHUNITHM NEW PLUS!! |
|
||||||
| 13 | CHUNITHM SUN |
|
| 13 | CHUNITHM SUN |
|
||||||
|
| 14 | CHUNITHM SUN PLUS |
|
||||||
|
|
||||||
|
|
||||||
### Importer
|
### Importer
|
||||||
@@ -54,16 +62,37 @@ Games listed below have been tested and confirmed working.
|
|||||||
In order to use the importer locate your game installation folder and execute:
|
In order to use the importer locate your game installation folder and execute:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
python read.py --game SDBT --version <version ID> --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder
|
python read.py --game SDBT --version <Version ID> --binfolder </path/to/game/data> --optfolder </path/to/game/option/folder>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Note: Use the /data not the /bin folder for the Importer!**
|
||||||
|
|
||||||
The importer for Chunithm will import: Events, Music, Charge Items and Avatar Accesories.
|
The importer for Chunithm will import: Events, Music, Charge Items and Avatar Accesories.
|
||||||
|
|
||||||
|
### Config
|
||||||
|
|
||||||
|
Config file is located in `config/chuni.yaml`.
|
||||||
|
|
||||||
|
| Option | Info |
|
||||||
|
|------------------|----------------------------------------------------------------------------------------------------------------|
|
||||||
|
| `news_msg` | If this is set, the news at the top of the main screen will be displayed (up to Chunithm Paradise Lost) |
|
||||||
|
| `name` | If this is set, all players that are not on a team will use this one by default. |
|
||||||
|
| `use_login_bonus`| This is used to enable the login bonuses |
|
||||||
|
| `crypto` | This option is used to enable the TLS Encryption |
|
||||||
|
|
||||||
|
|
||||||
|
**If you would like to use network encryption, the following will be required underneath but key, iv and hash are required:**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
crypto:
|
||||||
|
encrypted_only: False
|
||||||
|
keys:
|
||||||
|
13: ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000"]
|
||||||
|
```
|
||||||
|
|
||||||
### Database upgrade
|
### Database upgrade
|
||||||
|
|
||||||
Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see
|
Always make sure your database (tables) are up-to-date:
|
||||||
which version is the latest, f.e. `SDBT_4_upgrade.sql`. In order to upgrade to version 4 in this case you need to
|
|
||||||
perform all previous updates as well:
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
python dbutils.py --game SDBT upgrade
|
python dbutils.py --game SDBT upgrade
|
||||||
@@ -88,13 +117,37 @@ After a failed Online Battle the room will be deleted. The host is used for the
|
|||||||
- Timer countdown should be handled globally and not by one user
|
- Timer countdown should be handled globally and not by one user
|
||||||
- Game can freeze or can crash if someone (especially the host) leaves the matchmaking
|
- Game can freeze or can crash if someone (especially the host) leaves the matchmaking
|
||||||
|
|
||||||
|
### Rivals
|
||||||
|
|
||||||
|
You can configure up to 4 rivals in Chunithm on a per-user basis. There is no UI to do this currently, so in the database, you can do this:
|
||||||
|
```sql
|
||||||
|
INSERT INTO aime.chuni_item_favorite (user, version, favId, favKind) VALUES (<user1>, <version>, <user2>, 2);
|
||||||
|
INSERT INTO aime.chuni_item_favorite (user, version, favId, favKind) VALUES (<user2>, <version>, <user1>, 2);
|
||||||
|
```
|
||||||
|
Note that the version **must match**, otherwise song lookup may not work.
|
||||||
|
|
||||||
|
### Teams
|
||||||
|
|
||||||
|
You can also configure teams for users to be on. There is no UI to do this currently, so in the database, you can do this:
|
||||||
|
```sql
|
||||||
|
INSERT INTO aime.chuni_profile_team (teamName) VALUES (<teamName>);
|
||||||
|
```
|
||||||
|
Team names can be regular ASCII, and they will be displayed ingame.
|
||||||
|
|
||||||
|
### Favorite songs
|
||||||
|
You can set the songs that will be in a user's Favorite Songs category using the following SQL entries:
|
||||||
|
```sql
|
||||||
|
INSERT INTO aime.chuni_item_favorite (user, version, favId, favKind) VALUES (<user>, <version>, <songId>, 1);
|
||||||
|
```
|
||||||
|
The songId is based on the actual ID within your version of Chunithm.
|
||||||
|
|
||||||
|
|
||||||
## crossbeats REV.
|
## crossbeats REV.
|
||||||
|
|
||||||
### SDCA
|
### SDCA
|
||||||
|
|
||||||
| Version ID | Version Name |
|
| Version ID | Version Name |
|
||||||
|------------|------------------------------------|
|
| ---------- | ---------------------------------- |
|
||||||
| 0 | crossbeats REV. |
|
| 0 | crossbeats REV. |
|
||||||
| 1 | crossbeats REV. SUNRISE |
|
| 1 | crossbeats REV. SUNRISE |
|
||||||
| 2 | crossbeats REV. SUNRISE S2 |
|
| 2 | crossbeats REV. SUNRISE S2 |
|
||||||
@@ -105,7 +158,7 @@ After a failed Online Battle the room will be deleted. The host is used for the
|
|||||||
In order to use the importer you need to use the provided `Export.csv` file:
|
In order to use the importer you need to use the provided `Export.csv` file:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
python read.py --game SDCA --version <version ID> --binfolder titles/cxb/data
|
python read.py --game SDCA --version <Version ID> --binfolder titles/cxb/data
|
||||||
```
|
```
|
||||||
|
|
||||||
The importer for crossbeats REV. will import Music.
|
The importer for crossbeats REV. will import Music.
|
||||||
@@ -114,26 +167,12 @@ The importer for crossbeats REV. will import Music.
|
|||||||
|
|
||||||
Config file is located in `config/cxb.yaml`.
|
Config file is located in `config/cxb.yaml`.
|
||||||
|
|
||||||
| Option | Info |
|
|
||||||
|------------------------|------------------------------------------------------------|
|
|
||||||
| `hostname` | Requires a proper `hostname` (not localhost!) to run |
|
|
||||||
| `ssl_enable` | Enables/Disables the use of the `ssl_cert` and `ssl_key` |
|
|
||||||
| `port` | Set your unsecure port number |
|
|
||||||
| `port_secure` | Set your secure/SSL port number |
|
|
||||||
| `ssl_cert`, `ssl_key` | Enter your SSL certificate (requires not self signed cert) |
|
|
||||||
|
|
||||||
|
|
||||||
## maimai DX
|
## maimai DX
|
||||||
|
|
||||||
### SDEZ
|
### Versions
|
||||||
|
|
||||||
| Game Code | Version ID | Version Name |
|
| Game Code | Version ID | Version Name |
|
||||||
|-----------|------------|-------------------------|
|
| --------- | ---------- | ----------------------- |
|
||||||
|
|
||||||
|
|
||||||
For versions pre-dx
|
|
||||||
| Game Code | Version ID | Version Name |
|
|
||||||
|-----------|------------|-------------------------|
|
|
||||||
| SBXL | 0 | maimai |
|
| SBXL | 0 | maimai |
|
||||||
| SBXL | 1 | maimai PLUS |
|
| SBXL | 1 | maimai PLUS |
|
||||||
| SBZF | 2 | maimai GreeN |
|
| SBZF | 2 | maimai GreeN |
|
||||||
@@ -142,29 +181,31 @@ For versions pre-dx
|
|||||||
| SDBM | 5 | maimai ORANGE PLUS |
|
| SDBM | 5 | maimai ORANGE PLUS |
|
||||||
| SDCQ | 6 | maimai PiNK |
|
| SDCQ | 6 | maimai PiNK |
|
||||||
| SDCQ | 7 | maimai PiNK PLUS |
|
| SDCQ | 7 | maimai PiNK PLUS |
|
||||||
| SDDK | 8 | maimai MURASAKI |
|
| SDDK | 8 | maimai MURASAKi |
|
||||||
| SDDK | 9 | maimai MURASAKI PLUS |
|
| SDDK | 9 | maimai MURASAKi PLUS |
|
||||||
| SDDZ | 10 | maimai MILK |
|
| SDDZ | 10 | maimai MiLK |
|
||||||
| SDDZ | 11 | maimai MILK PLUS |
|
| SDDZ | 11 | maimai MiLK PLUS |
|
||||||
| SDEY | 12 | maimai FiNALE |
|
| SDEY | 12 | maimai FiNALE |
|
||||||
| SDEZ | 13 | maimai DX |
|
| SDEZ | 13 | maimai DX |
|
||||||
| SDEZ | 14 | maimai DX PLUS |
|
| SDEZ | 14 | maimai DX PLUS |
|
||||||
| SDEZ | 15 | maimai DX Splash |
|
| SDEZ | 15 | maimai DX Splash |
|
||||||
| SDEZ | 16 | maimai DX Splash PLUS |
|
| SDEZ | 16 | maimai DX Splash PLUS |
|
||||||
| SDEZ | 17 | maimai DX Universe |
|
| SDEZ | 17 | maimai DX UNiVERSE |
|
||||||
| SDEZ | 18 | maimai DX Universe PLUS |
|
| SDEZ | 18 | maimai DX UNiVERSE PLUS |
|
||||||
| SDEZ | 19 | maimai DX Festival |
|
| SDEZ | 19 | maimai DX FESTiVAL |
|
||||||
|
| SDEZ | 20 | maimai DX FESTiVAL PLUS |
|
||||||
|
| SDEZ | 21 | maimai DX BUDDiES |
|
||||||
|
|
||||||
### Importer
|
### Importer
|
||||||
|
|
||||||
In order to use the importer locate your game installation folder and execute:
|
In order to use the importer locate your game installation folder and execute:
|
||||||
DX:
|
DX:
|
||||||
```shell
|
```shell
|
||||||
python read.py --game <Game Code> --version <Version ID> --binfolder /path/to/StreamingAssets --optfolder /path/to/game/option/folder
|
python read.py --game <Game Code> --version <Version ID> --binfolder </path/to/Sinmai_Data> --optfolder </path/to/game/option/folder>
|
||||||
```
|
```
|
||||||
Pre-DX:
|
Pre-DX:
|
||||||
```shell
|
```shell
|
||||||
python read.py --game <Game Code> --version <Version ID> --binfolder /path/to/data --optfolder /path/to/patch/data
|
python read.py --game <Game Code> --version <Version ID> --binfolder </path/to/data> --optfolder </path/to/patch/data>
|
||||||
```
|
```
|
||||||
The importer for maimai DX will import Events, Music and Tickets.
|
The importer for maimai DX will import Events, Music and Tickets.
|
||||||
|
|
||||||
@@ -174,11 +215,12 @@ The importer for maimai Pre-DX will import Events and Music. Not all games will
|
|||||||
|
|
||||||
### Database upgrade
|
### Database upgrade
|
||||||
|
|
||||||
Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see which version is the latest, f.e. `SDEZ_2_upgrade.sql`. In order to upgrade to version 2 in this case you need to perform all previous updates as well:
|
Always make sure your database (tables) are up-to-date:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
python dbutils.py --game SDEZ upgrade
|
python dbutils.py --game SDEZ upgrade
|
||||||
```
|
```
|
||||||
|
|
||||||
Pre-Dx uses the same database as DX, so only upgrade using the SDEZ game code!
|
Pre-Dx uses the same database as DX, so only upgrade using the SDEZ game code!
|
||||||
|
|
||||||
## Hatsune Miku Project Diva
|
## Hatsune Miku Project Diva
|
||||||
@@ -186,7 +228,7 @@ Pre-Dx uses the same database as DX, so only upgrade using the SDEZ game code!
|
|||||||
### SBZV
|
### SBZV
|
||||||
|
|
||||||
| Version ID | Version Name |
|
| Version ID | Version Name |
|
||||||
|------------|---------------------------------|
|
| ---------- | ------------------------------- |
|
||||||
| 0 | Project Diva Arcade |
|
| 0 | Project Diva Arcade |
|
||||||
| 1 | Project Diva Arcade Future Tone |
|
| 1 | Project Diva Arcade Future Tone |
|
||||||
|
|
||||||
@@ -196,7 +238,7 @@ Pre-Dx uses the same database as DX, so only upgrade using the SDEZ game code!
|
|||||||
In order to use the importer locate your game installation folder and execute:
|
In order to use the importer locate your game installation folder and execute:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
python read.py --game SBZV --version <version ID> --binfolder /path/to/game/data/diva --optfolder /path/to/game/data/diva/mdata
|
python read.py --game SBZV --version <Version ID> --binfolder </path/to/game/data/diva> --optfolder </path/to/game/data/diva/mdata>
|
||||||
```
|
```
|
||||||
|
|
||||||
The importer for Project Diva Arcade will all required data in order to use
|
The importer for Project Diva Arcade will all required data in order to use
|
||||||
@@ -207,16 +249,17 @@ the Shop, Modules and Customizations.
|
|||||||
Config file is located in `config/diva.yaml`.
|
Config file is located in `config/diva.yaml`.
|
||||||
|
|
||||||
| Option | Info |
|
| Option | Info |
|
||||||
|----------------------|-------------------------------------------------------------------------------------------------|
|
| -------------------- | ----------------------------------------------------------------------------------------------- |
|
||||||
| `unlock_all_modules` | Unlocks all modules (costumes) by default, if set to `False` all modules need to be purchased |
|
| `unlock_all_modules` | Unlocks all modules (costumes) by default, if set to `False` all modules need to be purchased |
|
||||||
| `unlock_all_items` | Unlocks all items (customizations) by default, if set to `False` all items need to be purchased |
|
| `unlock_all_items` | Unlocks all items (customizations) by default, if set to `False` all items need to be purchased |
|
||||||
|
|
||||||
|
### Custom PV Lists (databanks)
|
||||||
|
|
||||||
|
In order to use custom PV Lists, simply drop in your .dat files inside of /titles/diva/data/ and make sure they are called PvList0.dat, PvList1.dat, PvList2.dat, PvList3.dat and PvList4.dat exactly.
|
||||||
|
|
||||||
### Database upgrade
|
### Database upgrade
|
||||||
|
|
||||||
Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see
|
Always make sure your database (tables) are up-to-date:
|
||||||
which version is the latest, f.e. `SBZV_4_upgrade.sql`. In order to upgrade to version 4 in this case you need to
|
|
||||||
perform all previous updates as well:
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
python dbutils.py --game SBZV upgrade
|
python dbutils.py --game SBZV upgrade
|
||||||
@@ -227,7 +270,7 @@ python dbutils.py --game SBZV upgrade
|
|||||||
### SDDT
|
### SDDT
|
||||||
|
|
||||||
| Version ID | Version Name |
|
| Version ID | Version Name |
|
||||||
|------------|----------------------------|
|
| ---------- | -------------------------- |
|
||||||
| 0 | O.N.G.E.K.I. |
|
| 0 | O.N.G.E.K.I. |
|
||||||
| 1 | O.N.G.E.K.I. + |
|
| 1 | O.N.G.E.K.I. + |
|
||||||
| 2 | O.N.G.E.K.I. SUMMER |
|
| 2 | O.N.G.E.K.I. SUMMER |
|
||||||
@@ -243,7 +286,7 @@ python dbutils.py --game SBZV upgrade
|
|||||||
In order to use the importer locate your game installation folder and execute:
|
In order to use the importer locate your game installation folder and execute:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
python read.py --game SDDT --version <version ID> --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder
|
python read.py --game SDDT --version <Version ID> --binfolder </path/to/game/mu3_Data> --optfolder </path/to/game/option/folder>
|
||||||
```
|
```
|
||||||
|
|
||||||
The importer for O.N.G.E.K.I. will all all Cards, Music and Events.
|
The importer for O.N.G.E.K.I. will all all Cards, Music and Events.
|
||||||
@@ -255,42 +298,119 @@ The importer for O.N.G.E.K.I. will all all Cards, Music and Events.
|
|||||||
Config file is located in `config/ongeki.yaml`.
|
Config file is located in `config/ongeki.yaml`.
|
||||||
|
|
||||||
| Option | Info |
|
| Option | Info |
|
||||||
|------------------|----------------------------------------------------------------------------------------------------------------|
|
| ---------------- | -------------------------------------------------------------------------------------------------------------- |
|
||||||
| `enabled_gachas` | Enter all gacha IDs for Card Maker to work, other than default may not work due to missing cards added to them |
|
| `enabled_gachas` | Enter all gacha IDs for Card Maker to work, other than default may not work due to missing cards added to them |
|
||||||
|
| `crypto` | This option is used to enable the TLS Encryption |
|
||||||
|
|
||||||
Note: 1149 and higher are only for Card Maker 1.35 and higher and will be ignored on lower versions.
|
Note: 1149 and higher are only for Card Maker 1.35 and higher and will be ignored on lower versions.
|
||||||
|
|
||||||
|
**If you would like to use network encryption, the following will be required underneath but key, iv and hash are required:**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
crypto:
|
||||||
|
encrypted_only: False
|
||||||
|
keys:
|
||||||
|
7: ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000"]
|
||||||
|
```
|
||||||
|
|
||||||
### Database upgrade
|
### Database upgrade
|
||||||
|
|
||||||
Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see
|
Always make sure your database (tables) are up-to-date:
|
||||||
which version is the latest, f.e. `SDDT_4_upgrade.sql`. In order to upgrade to version 4 in this case you need to
|
|
||||||
perform all previous updates as well:
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
python dbutils.py --game SDDT upgrade
|
python dbutils.py --game SDDT upgrade
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Controlling Events (Ranking Event, Technical Challenge Event, Mission Event)
|
||||||
|
|
||||||
|
Events are controlled by 2 types of enabled events:
|
||||||
|
- RankingEvent (type 6), TechChallengeEvent (type 17)
|
||||||
|
- AcceptRankingEvent (type 7), AcceptTechChallengeEvent (type 18)
|
||||||
|
|
||||||
|
Both Ranking and Accept must be enabled for event to function properly
|
||||||
|
|
||||||
|
Event will run for the time specified in startDate and endDate
|
||||||
|
|
||||||
|
AcceptRankingEvent and AcceptTechChallengeEvent are reward period for events, which specify from what startDate until endDate you can collect the rewards for attending the event, so the reward period must start in the future, e.g. :
|
||||||
|
|
||||||
|
- RankingEvent startDate 2023-12-01 - endDate 2023-12-30 - period in which whole event is running
|
||||||
|
- AcceptRankingEvent startDate 2023-12-23 - endDate 2023-12-30 - period in which you can collect rewards for the event
|
||||||
|
|
||||||
|
If player misses the AcceptRankingEvent period - ranking will be invalidated and receive lowest reward from the event (typically 500x money)
|
||||||
|
|
||||||
|
Technical Challenge Song List:
|
||||||
|
|
||||||
|
Songs that are used for Technical Challenge are not stored anywhere in data files, so you need to fill the database table by yourself, you can gather all songs that should be in Technical Challenges from ONGEKI japanese wikis, or, you can create your own sets:
|
||||||
|
|
||||||
|
Database table : `ongeki_static_tech_music`
|
||||||
|
|
||||||
|
```
|
||||||
|
id: Id in table, just increment for each entry
|
||||||
|
version: version of the game you want the tech challenge to be in (from RED and up)
|
||||||
|
eventId: Id of the event in ongeki_static_events, insert the Id of the TechChallengeEvent (type 17) you want the song be assigned to
|
||||||
|
musicId: Id of the song you want to add, use songId from ongeki_static_music table
|
||||||
|
level: Difficulty of the song you want to track during the event, from 0(basic) to 3(master)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Current implementation of Ranking and Technical Challenge Events are updated on every profile save to the Network, and Ranked on each player login, in official specification, calculation for current rank on the network should be done in the maintenance window
|
||||||
|
|
||||||
|
Mission Event (type 13) is a monthly type of event, which is used when another event doesn't have it's own Ranking or Technical Challenge Event running, only one Mission Event should be running at a time, so enable only the specific Mission you want to run currently on the Network
|
||||||
|
|
||||||
|
If you're often trying fresh cards, registering new profiles etc., you can also consider disabling all Announcement Events (type 1), as it will disable all the banners that pop up on login (they show up only once though, so if you click through them once they won't show again)
|
||||||
|
|
||||||
|
Event type 2 in Database are Advertisement Movies, enable only 1 you want to currently play, and disable others
|
||||||
|
|
||||||
|
|
||||||
|
Present and Reward List - populate reward list using read.py
|
||||||
|
|
||||||
|
Create present for players by adding an entry in `ongeki_static_present_list`
|
||||||
|
```
|
||||||
|
id: unique for each entry
|
||||||
|
version: game version you want the present be in
|
||||||
|
presentId: id of the present - starts with 1001 and go up from that, must be unique for each reward(don't set multiple rewardIds with same presentId)
|
||||||
|
presentName: present name which will be shown on the bottom when received
|
||||||
|
rewardId: ID of item from ongeki_static_rewards
|
||||||
|
stock: how many you want to give (like 5 copies of same card, or 10000 money, etc.)
|
||||||
|
message: no idea, can be left empty
|
||||||
|
startDate: date when to start giving out
|
||||||
|
endDate: date when ends
|
||||||
|
```
|
||||||
|
|
||||||
|
After inserting present to the table, add the presentId into players `ongeki_static_item`, where itemKind is 9, itemId is the presentId, and stock set 1 and isValid to 1
|
||||||
|
|
||||||
|
After that, on next login the present should be received (or whenever it supposed to happen)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Card Maker
|
## Card Maker
|
||||||
|
|
||||||
### SDED
|
### SDED
|
||||||
|
|
||||||
| Version ID | Version Name |
|
| Version ID | Version Name |
|
||||||
|------------|-----------------|
|
| ---------- | --------------- |
|
||||||
| 0 | Card Maker 1.30 |
|
| 0 | Card Maker 1.30 |
|
||||||
| 1 | Card Maker 1.35 |
|
| 1 | Card Maker 1.35 |
|
||||||
|
|
||||||
|
|
||||||
### Support status
|
### Support status
|
||||||
|
|
||||||
* Card Maker 1.30:
|
#### Card Maker 1.30:
|
||||||
* CHUNITHM NEW!!: Yes
|
* CHUNITHM NEW!!: Yes
|
||||||
* maimai DX UNiVERSE: Yes
|
* maimai DX UNiVERSE: Yes
|
||||||
* O.N.G.E.K.I. bright: Yes
|
* O.N.G.E.K.I. bright: Yes
|
||||||
|
|
||||||
* Card Maker 1.35:
|
#### Card Maker 1.35:
|
||||||
* CHUNITHM SUN: Yes (NEW PLUS!! up to A032)
|
* CHUNITHM:
|
||||||
* maimai DX FESTiVAL: Yes (up to A035) (UNiVERSE PLUS up to A031)
|
* NEW!!: Yes
|
||||||
* O.N.G.E.K.I. bright MEMORY: Yes
|
* NEW PLUS!!: Yes (added in A028)
|
||||||
|
* SUN: Yes (added in A032)
|
||||||
|
* maimai DX:
|
||||||
|
* UNiVERSE PLUS: Yes
|
||||||
|
* FESTiVAL: Yes (added in A031)
|
||||||
|
* FESTiVAL PLUS: Yes (added in A035)
|
||||||
|
* BUDDiES: Yes (added in A039)
|
||||||
|
* O.N.G.E.K.I. bright MEMORY: Yes
|
||||||
|
|
||||||
|
|
||||||
### Importer
|
### Importer
|
||||||
@@ -299,19 +419,19 @@ In order to use the importer you need to use the provided `.csv` files (which ar
|
|||||||
option folders:
|
option folders:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
python read.py --game SDED --version <version ID> --binfolder titles/cm/cm_data --optfolder /path/to/cardmaker/option/folder
|
python read.py --game SDED --version <Version ID> --binfolder titles/cm/cm_data --optfolder </path/to/cardmaker/option/folder>
|
||||||
```
|
```
|
||||||
|
|
||||||
**If you haven't already executed the O.N.G.E.K.I. importer, make sure you import all cards!**
|
**If you haven't already executed the O.N.G.E.K.I. importer, make sure you import all cards!**
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
python read.py --game SDDT --version <version ID> --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder
|
python read.py --game SDDT --version <Version ID> --binfolder </path/to/ongeki/mu3_Data> --optfolder </path/to/ongeki/option/folder>
|
||||||
```
|
```
|
||||||
|
|
||||||
Also make sure to import all maimai DX and CHUNITHM data as well:
|
Also make sure to import all maimai DX and CHUNITHM data as well:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
python read.py --game SDED --version <version ID> --binfolder /path/to/cardmaker/CardMaker_Data
|
python read.py --game SDED --version <Version ID> --binfolder </path/to/cardmaker/CardMaker_Data> --optfolder </path/to/cardmaker/option/folder>
|
||||||
```
|
```
|
||||||
|
|
||||||
The importer for Card Maker will import all required Gachas (Banners) and cards (for maimai DX/CHUNITHM) and the hardcoded
|
The importer for Card Maker will import all required Gachas (Banners) and cards (for maimai DX/CHUNITHM) and the hardcoded
|
||||||
@@ -391,7 +511,7 @@ Gacha IDs up to 1140 will be loaded for CM 1.34 and all gachas will be loaded fo
|
|||||||
### SDFE
|
### SDFE
|
||||||
|
|
||||||
| Version ID | Version Name |
|
| Version ID | Version Name |
|
||||||
|------------|---------------|
|
| ---------- | ------------- |
|
||||||
| 0 | WACCA |
|
| 0 | WACCA |
|
||||||
| 1 | WACCA S |
|
| 1 | WACCA S |
|
||||||
| 2 | WACCA Lily |
|
| 2 | WACCA Lily |
|
||||||
@@ -404,7 +524,7 @@ Gacha IDs up to 1140 will be loaded for CM 1.34 and all gachas will be loaded fo
|
|||||||
In order to use the importer locate your game installation folder and execute:
|
In order to use the importer locate your game installation folder and execute:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
python read.py --game SDFE --version <version ID> --binfolder /path/to/game/WindowsNoEditor/Mercury/Content
|
python read.py --game SDFE --version <Version ID> --binfolder </path/to/game/WindowsNoEditor/Mercury/Content>
|
||||||
```
|
```
|
||||||
|
|
||||||
The importer for WACCA will import all Music data.
|
The importer for WACCA will import all Music data.
|
||||||
@@ -414,7 +534,7 @@ The importer for WACCA will import all Music data.
|
|||||||
Config file is located in `config/wacca.yaml`.
|
Config file is located in `config/wacca.yaml`.
|
||||||
|
|
||||||
| Option | Info |
|
| Option | Info |
|
||||||
|--------------------|-----------------------------------------------------------------------------|
|
| ------------------ | --------------------------------------------------------------------------- |
|
||||||
| `always_vip` | Enables/Disables VIP, if disabled it needs to be purchased manually in game |
|
| `always_vip` | Enables/Disables VIP, if disabled it needs to be purchased manually in game |
|
||||||
| `infinite_tickets` | Always set the "unlock expert" tickets to 5 |
|
| `infinite_tickets` | Always set the "unlock expert" tickets to 5 |
|
||||||
| `infinite_wp` | Sets the user WP to `999999` |
|
| `infinite_wp` | Sets the user WP to `999999` |
|
||||||
@@ -423,7 +543,7 @@ Config file is located in `config/wacca.yaml`.
|
|||||||
|
|
||||||
### Database upgrade
|
### Database upgrade
|
||||||
|
|
||||||
Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see which version is the latest, f.e. `SDFE_3_upgrade.sql`. In order to upgrade to version 3 in this case you need to perform all previous updates as well:
|
Always make sure your database (tables) are up-to-date:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
python dbutils.py --game SDFE upgrade
|
python dbutils.py --game SDFE upgrade
|
||||||
@@ -468,9 +588,9 @@ Below is a list of VIP rewards. Currently, VIP is not implemented, and thus thes
|
|||||||
|
|
||||||
### SDEW
|
### SDEW
|
||||||
|
|
||||||
| Version ID | Version Name |
|
| Version ID | Version Name |
|
||||||
|------------|---------------|
|
| ---------- | ------------ |
|
||||||
| 0 | SAO |
|
| 0 | SAO |
|
||||||
|
|
||||||
|
|
||||||
### Importer
|
### Importer
|
||||||
@@ -478,7 +598,7 @@ Below is a list of VIP rewards. Currently, VIP is not implemented, and thus thes
|
|||||||
In order to use the importer locate your game installation folder and execute:
|
In order to use the importer locate your game installation folder and execute:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
python read.py --game SDEW --version <version ID> --binfolder /path/to/game/extractedassets
|
python read.py --game SDEW --version 0 --binfolder /titles/sao/data/
|
||||||
```
|
```
|
||||||
|
|
||||||
The importer for SAO will import all items, heroes, support skills and titles data.
|
The importer for SAO will import all items, heroes, support skills and titles data.
|
||||||
@@ -487,16 +607,16 @@ The importer for SAO will import all items, heroes, support skills and titles da
|
|||||||
|
|
||||||
Config file is located in `config/sao.yaml`.
|
Config file is located in `config/sao.yaml`.
|
||||||
|
|
||||||
| Option | Info |
|
| Option | Info |
|
||||||
|--------------------|-----------------------------------------------------------------------------|
|
| --------------- | ----------------------------------------------------------------- |
|
||||||
| `hostname` | Changes the server listening address for Mucha |
|
| `hostname` | Changes the server listening address for Mucha |
|
||||||
| `port` | Changes the listing port |
|
| `port` | Changes the listing port |
|
||||||
| `auto_register` | Allows the game to handle the automatic registration of new cards |
|
| `auto_register` | Allows the game to handle the automatic registration of new cards |
|
||||||
|
|
||||||
|
|
||||||
### Database upgrade
|
### Database upgrade
|
||||||
|
|
||||||
Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see which version is the latest, f.e. `SDEW_1_upgrade.sql`. In order to upgrade to version 3 in this case you need to perform all previous updates as well:
|
Always make sure your database (tables) are up-to-date:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
python dbutils.py --game SDEW upgrade
|
python dbutils.py --game SDEW upgrade
|
||||||
@@ -505,12 +625,207 @@ python dbutils.py --game SDEW upgrade
|
|||||||
### Notes
|
### Notes
|
||||||
- Defrag Match will crash at loading
|
- Defrag Match will crash at loading
|
||||||
- Co-Op Online is not supported
|
- Co-Op Online is not supported
|
||||||
- Shop is not functionnal
|
- Shop is displayed but cannot purchase heroes or items
|
||||||
- Player title is currently static and cannot be changed in-game
|
- Player title is currently static and cannot be changed in-game
|
||||||
- QR Card Scanning currently only load a static hero
|
- QR Card Scanning currently only load a static hero
|
||||||
|
- Ex-quests progression not supported yet
|
||||||
|
- Daily Missions not implemented
|
||||||
|
- EX TOWER 1,2 & 3 are not yet supported
|
||||||
|
- Daily Yui coin not yet fixed
|
||||||
|
|
||||||
### Credits for SAO support:
|
### Credits for SAO support:
|
||||||
|
|
||||||
- Midorica - Limited Network Support
|
- Midorica - Network Support
|
||||||
- Dniel97 - Helping with network base
|
- Dniel97 - Helping with network base
|
||||||
- tungnotpunk - Source
|
- tungnotpunk - Source
|
||||||
|
|
||||||
|
## Initial D THE ARCADE
|
||||||
|
|
||||||
|
### SDGT
|
||||||
|
|
||||||
|
| Version ID | Version Name |
|
||||||
|
| ---------- | ----------------------------- |
|
||||||
|
| 0 | Initial D THE ARCADE Season 1 |
|
||||||
|
| 1 | Initial D THE ARCADE Season 2 |
|
||||||
|
|
||||||
|
**Important: Only version 1.50.00 (Season 2) is currently working and actively supported!**
|
||||||
|
|
||||||
|
### Profile Importer
|
||||||
|
|
||||||
|
In order to use the profile importer download the `idac_profile.json` file from the frontend
|
||||||
|
and either directly use the folder path with `idac_profile.json` in it or specify the complete
|
||||||
|
path to the `.json` file
|
||||||
|
|
||||||
|
```shell
|
||||||
|
python read.py --game SDGT --version <Version ID> --optfolder /path/to/game/download/folder
|
||||||
|
```
|
||||||
|
|
||||||
|
The importer for SDGT will import the complete profile data with personal high scores as well.
|
||||||
|
|
||||||
|
### Config
|
||||||
|
|
||||||
|
Config file is located in `config/idac.yaml`.
|
||||||
|
|
||||||
|
| Option | Info |
|
||||||
|
| ----------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `ssl` | Enables/Disables the use of the `ssl_cert` and `ssl_key` (currently unsuported) |
|
||||||
|
| `matching_host` | IPv4 address of your PC for the Online Battle (currently unsupported) |
|
||||||
|
| `port_matching` | Port number for the Online Battle Matching |
|
||||||
|
| `port_echo1/2` | Port numbers for Echos |
|
||||||
|
| `port_matching_p2p` | Port number for Online Battle (currently unsupported) |
|
||||||
|
| `stamp.enable` | Enables/Disabled the play stamp events |
|
||||||
|
| `stamp.enabled_stamps` | Define up to 3 play stamp events (without `.json` extension, which are placed in `titles/idac/data/stamps`) |
|
||||||
|
| `timetrial.enable` | Enables/Disables the time trial event |
|
||||||
|
| `timetrial.enabled_timetrial` | Define one! trial event (without `.json` extension, which are placed in `titles/idac/data/timetrial`) |
|
||||||
|
|
||||||
|
|
||||||
|
### Database upgrade
|
||||||
|
|
||||||
|
Always make sure your database (tables) are up-to-date:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
python dbutils.py --game SDGT upgrade
|
||||||
|
```
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- Online Battle is not supported
|
||||||
|
- Online Battle Matching is not supported
|
||||||
|
|
||||||
|
### Item categories
|
||||||
|
|
||||||
|
| Category ID | Category Name |
|
||||||
|
| ----------- | ------------------------ |
|
||||||
|
| 1 | D Coin |
|
||||||
|
| 3 | Car Dressup Token |
|
||||||
|
| 5 | Avatar Dressup Token |
|
||||||
|
| 6 | Tachometer |
|
||||||
|
| 7 | Aura |
|
||||||
|
| 8 | Aura Color |
|
||||||
|
| 9 | Avatar Face |
|
||||||
|
| 10 | Avatar Eye |
|
||||||
|
| 11 | Avatar Mouth |
|
||||||
|
| 12 | Avatar Hair |
|
||||||
|
| 13 | Avatar Glasses |
|
||||||
|
| 14 | Avatar Face accessories |
|
||||||
|
| 15 | Avatar Body |
|
||||||
|
| 18 | Avatar Background |
|
||||||
|
| 21 | Chat Stamp |
|
||||||
|
| 22 | Keychain |
|
||||||
|
| 24 | Title |
|
||||||
|
| 25 | FullTune Ticket |
|
||||||
|
| 26 | Paper Cup |
|
||||||
|
| 27 | BGM |
|
||||||
|
| 28 | Drifting Text |
|
||||||
|
| 31 | Start Menu BG |
|
||||||
|
| 32 | Car Color/Paint |
|
||||||
|
| 33 | Aura Level |
|
||||||
|
| 34 | FullTune Ticket Fragment |
|
||||||
|
| 35 | Underneon Lights |
|
||||||
|
|
||||||
|
### TimeRelease Chapter:
|
||||||
|
|
||||||
|
1. Story: 1, 2, 3, 4, 5, 6, 7, 8, 9, 19 (Chapter 10), (29 Chapter 11?)
|
||||||
|
2. MF Ghost: 10, 11, 12, 13, 14, 15
|
||||||
|
3. Bunta: 15, 16, 17, 18, 19, 20, (21, 21, 22?)
|
||||||
|
4. Special Event: 23, 24, 25, 26, 27, 28 (Touhou Project)
|
||||||
|
|
||||||
|
### TimeRelease Courses:
|
||||||
|
|
||||||
|
| Course ID | Course Name | Direction |
|
||||||
|
| --------- | ------------------------- | ------------------------ |
|
||||||
|
| 0 | Lake Akina(秋名湖) | CounterClockwise(左周り) |
|
||||||
|
| 2 | Lake Akina(秋名湖) | Clockwise(右周り) |
|
||||||
|
| 52 | Hakone(箱根) | Downhill(下り) |
|
||||||
|
| 54 | Hakone(箱根) | Hillclimb(上り) |
|
||||||
|
| 36 | Usui(碓氷) | CounterClockwise(左周り) |
|
||||||
|
| 38 | Usui(碓氷) | Clockwise(右周り) |
|
||||||
|
| 4 | Myogi(妙義) | Downhill(下り) |
|
||||||
|
| 6 | Myogi(妙義) | Hillclimb(上り) |
|
||||||
|
| 8 | Akagi(赤城) | Downhill(下り) |
|
||||||
|
| 10 | Akagi(赤城) | Hillclimb(上り) |
|
||||||
|
| 12 | Akina(秋名) | Downhill(下り) |
|
||||||
|
| 14 | Akina(秋名) | Hillclimb(上り) |
|
||||||
|
| 16 | Irohazaka(いろは坂) | Downhill(下り) |
|
||||||
|
| 18 | Irohazaka(いろは坂) | Reverse(逆走) |
|
||||||
|
| 20 | Tsukuba(筑波) | Outbound(往路) |
|
||||||
|
| 22 | Tsukuba(筑波) | Inbound(復路) |
|
||||||
|
| 56 | Momiji Line(もみじライン) | Downhill(下り) |
|
||||||
|
| 58 | Momiji Line(もみじライン) | Hillclimb(上り) |
|
||||||
|
| 24 | Happogahara(八方ヶ原) | Outbound(往路) |
|
||||||
|
| 26 | Happogahara(八方ヶ原) | Inbound(復路) |
|
||||||
|
| 40 | Sadamine(定峰) | Downhill(下り) |
|
||||||
|
| 42 | Sadamine(定峰) | Hillclimb(上り) |
|
||||||
|
| 44 | Tsuchisaka(土坂) | Outbound(往路) |
|
||||||
|
| 46 | Tsuchisaka(土坂) | Inbound(復路) |
|
||||||
|
| 48 | Akina Snow(秋名雪) | Downhill(下り) |
|
||||||
|
| 50 | Akina Snow(秋名雪) | Hillclimb(上り) |
|
||||||
|
| 68 | Odawara(小田原) | Forward(順走) |
|
||||||
|
| 70 | Odawara(小田原) | Reverse(逆走) |
|
||||||
|
|
||||||
|
|
||||||
|
### TimeRelease `announce_image`:
|
||||||
|
|
||||||
|
- `save_filename`: Filename without file extension saved in the folder `ImageDelivery`
|
||||||
|
- `url`: URL to the file on the server with the corresponding file extension (.djg/.gpg)
|
||||||
|
(except for `display_id=9` where the url is empty)
|
||||||
|
- `open_dt`: UNIX timestamp when it should be displayed
|
||||||
|
- `close_dt`: UNIX timestamp when it should be hidden
|
||||||
|
- `display_id`: One of the following IDS:
|
||||||
|
|
||||||
|
| Display ID | Description |
|
||||||
|
| ---------------------------------------- | ------------------------------------------------------------------------------------------------- |
|
||||||
|
| 1 | ADV image in the size 1920x1080, shown during attract |
|
||||||
|
| 2 | Start image in the size 1280x720, shown in the Main Menu after selection the corresponding banner |
|
||||||
|
| 3 | Banner image in the size 640×120, shown in the Main Menu |
|
||||||
|
| 5 | Stamp Background image in the size 1780x608 |
|
||||||
|
| 6 | Online Battle round image in the size 1920x1080 |
|
||||||
|
| 8 | Stamp Pickup image in the size 624x300, also requires `target_id` set |
|
||||||
|
| 9 | Attract video from the `C:/Mount/Option` folder on real hardware, also requires a `target_id` set |
|
||||||
|
|
||||||
|
- `target_id`:
|
||||||
|
- Always 0 unless:
|
||||||
|
- `display_id=8`: Matches an existing stamp pickup abolsute `reward_setting_masu`
|
||||||
|
and will replace the stock image with the provided one from `url`
|
||||||
|
- `display_id=9`: Matches the id from `C:/Mount/Option/MV01/targetXXX.bin`,
|
||||||
|
where XXX is the `target_id`
|
||||||
|
- `page`:
|
||||||
|
- Defines the order in which the images being shown, where 1 is the first image
|
||||||
|
- `display_id` 1, 2, 3: The `page` has to match, so the corresponding images
|
||||||
|
of an event are shown correctly
|
||||||
|
- `display_id` 7, 8: The `page` defines the `sheet_design` in the play stamps
|
||||||
|
- `time`: The time in sec for an image to be shown, always 10
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"save_filename": "adv_01_example",
|
||||||
|
"url": "http://example.com/images/04721D5D3595FD29778011EC73A8AE77.dpg",
|
||||||
|
"open_dt": 1514761200,
|
||||||
|
"close_dt": 1861916400,
|
||||||
|
"display_id": 1,
|
||||||
|
"target_id": 0,
|
||||||
|
"page": 1,
|
||||||
|
"time": 10
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
### Battle Gift
|
||||||
|
|
||||||
|
- `gift_id`: unique gift index (starts from 0 f.e.)
|
||||||
|
- `battle_gift_event_id`: unique event id
|
||||||
|
- `reward_category`: item category (f.e. 21 = Chat Stamp)
|
||||||
|
- `reward_type`: item id (f.e. 483 = Remilia Scarlet)
|
||||||
|
- `reward_name`: name of the reward
|
||||||
|
- `rarity`: 2 Golden, 1 Silver, 3 Bronze
|
||||||
|
- `cash_rate`: ?
|
||||||
|
- `customize_point_rate`: ?
|
||||||
|
- `avatar_point_rate`: ?
|
||||||
|
- `first_distribution_rate`: only used server side for the probability of the first distribution
|
||||||
|
|
||||||
|
### Credits:
|
||||||
|
|
||||||
|
- Bottersnike: For the HUGE Reverse Engineering help
|
||||||
|
- Kinako: For helping with the timeRelease unlocking of courses and special mode
|
||||||
|
|
||||||
|
A huge thanks to all people who helped shaping this project to what it is now and don't want to be mentioned here.
|
||||||
|
|
||||||
|
|||||||
67
docs/prod.md
67
docs/prod.md
@@ -1,41 +1,34 @@
|
|||||||
# ARTEMiS Production mode
|
# ARTEMiS Production mode
|
||||||
Production mode is a configuration option that changes how the server listens to be more friendly to a production environment. This mode assumes that a proxy (for this guide, nginx) is standing in front of the server to handle port mapping and TLS. In order to activate production mode, simply change `is_develop` to `False` in `core.yaml`. Next time you start the server, you should see "Starting server in production mode".
|
ARTEMiS is designed to run in one of two ways. Developmen/local mode, which assumes you're just trying to set up something to save your scores and make the games work, and have patched your games to disable SSL and cert checks and encryption and the like, and production mode. In production mode, artemis assumes you have a proxy server, such as nginx or apache, standing in front of artemis doing HTTPS and port management. This document will cover how to properly set up a production instance of ARTEMiS.
|
||||||
|
|
||||||
|
## ARTEMiS configuration
|
||||||
|
Step 1 is to edit your artemis configuration. Some recomended changes:
|
||||||
|
### `server`
|
||||||
|
- `listen_address` -> `127.0.0.1`
|
||||||
|
- `is_develop` -> `False`
|
||||||
|
- `is_using_proxy` -> `True`
|
||||||
|
- `port` -> The port nginx will send proxied requests to. If you're using the example config, set this to 8080.
|
||||||
|
- `proxy_port` -> The port your proxy will be accepting title server connections on. If you're using the example config, set this to 80.
|
||||||
|
- `proxy_port_ssl` -> The port your proxy will be accepting secure title server connections on. If you're using the example config, set this to 443.
|
||||||
|
- `allow_unregistered_serials` -> `False`
|
||||||
|
### `billing`
|
||||||
|
- `standalone` -> `False`
|
||||||
|
### `allnet`
|
||||||
|
- `standalone` -> `False`
|
||||||
|
### `frontend`
|
||||||
|
- `enable` -> `True` if you want the frontend
|
||||||
|
- `port` -> `8090` if you're using the default nginx config, otherwise whatever port your proxy will be sending requests to
|
||||||
|
### `aimedb`
|
||||||
|
- `listen_address` -> `0.0.0.0` unless you're proxying aimedb requests (not recomended at this time), in which case, leave this option unchanged
|
||||||
|
|
||||||
|
If you plan to serve artemis behind a VPN, these additional settings are also recomended
|
||||||
|
- `check_arcade_ip` -> `True`
|
||||||
|
- `strict_ip_checking` -> `True`
|
||||||
|
|
||||||
## Nginx Configuration
|
## Nginx Configuration
|
||||||
### Port forwarding
|
For most cases, the config in `example_config` will suffice. It makes the following assumptions
|
||||||
Artemis requires that the following ports be forwarded to allow internet traffic to access the server. This will not change regardless of what you set in the config, as many of these ports are hard-coded in the games.
|
- ARTEMiS is running on port 8080
|
||||||
`tcp:80` all.net, non-ssl titles
|
- Billing is set to not be standalone
|
||||||
`tcp:8443` billing
|
- You're not using cloudflare in front of your frontend
|
||||||
`tcp:22345` aimedb
|
|
||||||
`tcp:443` frontend, SSL titles
|
|
||||||
|
|
||||||
### A note about external proxy services (cloudflare, etc)
|
If this describes you, your only configuration needs are to edit the `server_name` and `certificate_*` directives. Otherwise, please see nginx configuration documentation to configure it to best suit your setup.
|
||||||
Due to the way that artemis functions, it is currently not possible to put the server behind something like Cloudflare. Cloudflare only proxies web traffic on the standard ports (80, 443) and, as shown above, this does not work with artemis. Server administrators should seek other means to protect their network (VPS hosting, VPN, etc)
|
|
||||||
|
|
||||||
### SSL Certificates
|
|
||||||
You will need to generate SSL certificates for some games. The certificates vary in security and validity requirements. Please see the general guide below
|
|
||||||
- General Title: The certificate for the general title server should be valid, not self-signed and match the CN that the game will be reaching out to (e.i if your games are reaching out to titles.hostname.here, your ssl certificate should be valid for titles.hostname.here, or *.hostname.here)
|
|
||||||
- CXB: Same requires as the title server. It must not be self-signed, and CN must match. Recomended to get a wildcard cert if possible, and use it for both Title and CXB
|
|
||||||
- Pokken: Pokken can be self-signed, and the CN doesn't have to match, but it MUST use 2048-bit RSA. Due to the games age, andthing stronger then that will be rejected.
|
|
||||||
|
|
||||||
### Port mappings
|
|
||||||
An example config is provided in the `config` folder called `nginx_example.conf`. It is set up for the following:
|
|
||||||
`naominet.jp:tcp:80` -> `localhost:tcp:8000` for allnet
|
|
||||||
`ib.naominet.jp:ssl:8443` -> `localhost:tcp:8444` for the billing server
|
|
||||||
`your.hostname.here:ssl:443` -> `localhost:tcp:8080` for the SSL title server
|
|
||||||
`your.hostname.here:tcp:80` -> `localhost:tcp:8080` for the non-SSL title server
|
|
||||||
`cxb.hostname.here:ssl:443` -> `localhost:tcp:8080` for crossbeats (appends /SDCA/104/ to the request)
|
|
||||||
`pokken.hostname.here:ssl:443` -> `localhost:tcp:8080` for pokken
|
|
||||||
`frontend.hostname.here:ssl:443` -> `localhost:tcp:8090` for the frontend, includes https redirection
|
|
||||||
|
|
||||||
If you're using this as a guide, be sure to replace your.hostname.here with the hostname you specified in core.yaml under `titles->hostname`. Do *not* change naominet.jp, or allnet/billing will fail. Also remember to specifiy certificate paths correctly, as in the example they are simply placeholders.
|
|
||||||
|
|
||||||
### Multi-service ports
|
|
||||||
It is possible to use nginx to redirect billing and title server requests to the same port that all.net uses. By setting `port` to 0 under billing and title server, you can change the nginx config to serve the following (entries not shown here should be the same)
|
|
||||||
`ib.naominet.jp:ssl:8443` -> `localhost:tcp:8000` for the billing server
|
|
||||||
`your.hostname.here:ssl:443` -> `localhost:tcp:8000` for the SSL title server
|
|
||||||
`your.hostname.here:tcp:80` -> `localhost:tcp:8000` for the non-SSL title server
|
|
||||||
`cxb.hostname.here:ssl:443` -> `localhost:tcp:8000` for crossbeats (appends /SDCA/104/ to the request)
|
|
||||||
`pokken.hostname.here:ssl:443` -> `localhost:tcp:8000` for pokken
|
|
||||||
|
|
||||||
This will allow you to only use 3 ports locally, but you will still need to forward the same internet-facing ports as before.
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
server:
|
server:
|
||||||
enable: True
|
enable: True
|
||||||
loglevel: "info"
|
loglevel: "info"
|
||||||
|
news_msg: ""
|
||||||
|
|
||||||
team:
|
team:
|
||||||
name: ARTEMiS
|
name: ARTEMiS # If this is set, all players that are not on a team will use this one by default.
|
||||||
|
|
||||||
mods:
|
mods:
|
||||||
use_login_bonus: True
|
use_login_bonus: True
|
||||||
@@ -18,6 +19,14 @@ version:
|
|||||||
13:
|
13:
|
||||||
rom: 2.10.00
|
rom: 2.10.00
|
||||||
data: 2.10.00
|
data: 2.10.00
|
||||||
|
14:
|
||||||
|
rom: 2.15.00
|
||||||
|
data: 2.15.00
|
||||||
|
|
||||||
crypto:
|
crypto:
|
||||||
encrypted_only: False
|
encrypted_only: False
|
||||||
|
|
||||||
|
matching:
|
||||||
|
enable: False
|
||||||
|
match_time_limit: 60
|
||||||
|
match_error_limit: 9999
|
||||||
|
|||||||
@@ -1,18 +1,24 @@
|
|||||||
server:
|
server:
|
||||||
listen_address: "127.0.0.1"
|
listen_address: "127.0.0.1"
|
||||||
|
hostname: "localhost"
|
||||||
|
port: 80
|
||||||
|
ssl_key: "cert/title.key"
|
||||||
|
ssl_cert: "cert/title.crt"
|
||||||
allow_user_registration: True
|
allow_user_registration: True
|
||||||
allow_unregistered_serials: True
|
allow_unregistered_serials: True
|
||||||
name: "ARTEMiS"
|
name: "ARTEMiS"
|
||||||
is_develop: True
|
is_develop: True
|
||||||
threading: False
|
is_using_proxy: False
|
||||||
|
proxy_port: 0
|
||||||
|
proxy_port_ssl: 0
|
||||||
log_dir: "logs"
|
log_dir: "logs"
|
||||||
check_arcade_ip: False
|
check_arcade_ip: False
|
||||||
strict_ip_checking: False
|
strict_ip_checking: False
|
||||||
|
|
||||||
title:
|
title:
|
||||||
loglevel: "info"
|
loglevel: "info"
|
||||||
hostname: "localhost"
|
reboot_start_time: "04:00"
|
||||||
port: 8080
|
reboot_end_time: "05:00"
|
||||||
|
|
||||||
database:
|
database:
|
||||||
host: "localhost"
|
host: "localhost"
|
||||||
@@ -22,34 +28,39 @@ database:
|
|||||||
port: 3306
|
port: 3306
|
||||||
protocol: "mysql"
|
protocol: "mysql"
|
||||||
sha2_password: False
|
sha2_password: False
|
||||||
loglevel: "warn"
|
loglevel: "info"
|
||||||
user_table_autoincrement_start: 10000
|
enable_memcached: True
|
||||||
memcached_host: "localhost"
|
memcached_host: "localhost"
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
enable: False
|
enable: False
|
||||||
port: 8090
|
port: 8080
|
||||||
loglevel: "info"
|
loglevel: "info"
|
||||||
|
secret: ""
|
||||||
|
|
||||||
allnet:
|
allnet:
|
||||||
loglevel: "info"
|
standalone: False
|
||||||
port: 80
|
port: 80
|
||||||
ip_check: False
|
loglevel: "info"
|
||||||
allow_online_updates: False
|
allow_online_updates: False
|
||||||
update_cfg_folder: ""
|
update_cfg_folder: ""
|
||||||
|
|
||||||
billing:
|
billing:
|
||||||
|
standalone: True
|
||||||
|
loglevel: "info"
|
||||||
port: 8443
|
port: 8443
|
||||||
ssl_key: "cert/server.key"
|
ssl_key: "cert/server.key"
|
||||||
ssl_cert: "cert/server.pem"
|
ssl_cert: "cert/server.pem"
|
||||||
signing_key: "cert/billing.key"
|
signing_key: "cert/billing.key"
|
||||||
|
|
||||||
aimedb:
|
aimedb:
|
||||||
|
enable: True
|
||||||
|
listen_address: ""
|
||||||
loglevel: "info"
|
loglevel: "info"
|
||||||
port: 22345
|
port: 22345
|
||||||
key: ""
|
key: ""
|
||||||
|
id_secret: ""
|
||||||
|
id_lifetime_seconds: 86400
|
||||||
|
|
||||||
mucha:
|
mucha:
|
||||||
enable: False
|
|
||||||
hostname: "localhost"
|
|
||||||
loglevel: "info"
|
loglevel: "info"
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
server:
|
server:
|
||||||
enable: True
|
enable: True
|
||||||
loglevel: "info"
|
loglevel: "info"
|
||||||
hostname: "localhost"
|
use:https: True
|
||||||
ssl_enable: False
|
|
||||||
port: 8082
|
|
||||||
port_secure: 443
|
|
||||||
ssl_cert: "cert/title.crt"
|
|
||||||
ssl_key: "cert/title.key"
|
|
||||||
3
example_config/fgoa.yaml
Normal file
3
example_config/fgoa.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
server:
|
||||||
|
enable: True
|
||||||
|
loglevel: "info"
|
||||||
30
example_config/idac.yaml
Normal file
30
example_config/idac.yaml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
server:
|
||||||
|
enable: True
|
||||||
|
loglevel: "info"
|
||||||
|
ssl: False
|
||||||
|
ssl_key: "cert/idac.key"
|
||||||
|
ssl_cert: "cert/idac.crt"
|
||||||
|
matching_host: "127.0.0.1"
|
||||||
|
port_matching: 20000
|
||||||
|
port_echo1: 20001
|
||||||
|
port_echo2: 20002
|
||||||
|
port_matching_p2p: 20003
|
||||||
|
|
||||||
|
timerelease:
|
||||||
|
timerelease_no: 3
|
||||||
|
timerelease_avatar_gacha_no: 3
|
||||||
|
|
||||||
|
stamp:
|
||||||
|
enable: True
|
||||||
|
enabled_stamps: # max 3 play stamps
|
||||||
|
- "touhou_remilia_scarlet"
|
||||||
|
- "touhou_flandre_scarlet"
|
||||||
|
- "touhou_sakuya_izayoi"
|
||||||
|
|
||||||
|
timetrial:
|
||||||
|
enable: True
|
||||||
|
enabled_timetrial: "touhou_remilia_scarlet"
|
||||||
|
|
||||||
|
battle_event:
|
||||||
|
enabled: True
|
||||||
|
enabled_battle_event: "touhou_1st"
|
||||||
@@ -6,7 +6,7 @@ server {
|
|||||||
location / {
|
location / {
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_pass_request_headers on;
|
proxy_pass_request_headers on;
|
||||||
proxy_pass http://localhost:8000/;
|
proxy_pass http://127.0.0.1:8080/;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,14 +18,13 @@ server {
|
|||||||
location / {
|
location / {
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_pass_request_headers on;
|
proxy_pass_request_headers on;
|
||||||
proxy_pass http://localhost:8080/;
|
proxy_pass http://127.0.0.1:8080/;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# SSL titles, comment out if you don't plan on accepting SSL titles
|
# SSL titles, comment out if you don't plan on accepting SSL titles
|
||||||
server {
|
server {
|
||||||
listen 443 ssl default_server;
|
listen 443 ssl;
|
||||||
listen [::]:443 ssl default_server;
|
|
||||||
server_name your.hostname.here;
|
server_name your.hostname.here;
|
||||||
|
|
||||||
ssl_certificate /path/to/cert/title.crt;
|
ssl_certificate /path/to/cert/title.crt;
|
||||||
@@ -39,11 +38,13 @@ server {
|
|||||||
ssl_prefer_server_ciphers off;
|
ssl_prefer_server_ciphers off;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://localhost:8080/;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_pass_request_headers on;
|
||||||
|
proxy_pass http://127.0.0.1:8080/;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Billing
|
# Billing, comment this out if running billing standalone
|
||||||
server {
|
server {
|
||||||
listen 8443 ssl;
|
listen 8443 ssl;
|
||||||
server_name ib.naominet.jp;
|
server_name ib.naominet.jp;
|
||||||
@@ -55,55 +56,13 @@ server {
|
|||||||
ssl_session_tickets off;
|
ssl_session_tickets off;
|
||||||
|
|
||||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
|
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
|
||||||
ssl_ciphers "ALL:@SECLEVEL=1";
|
ssl_ciphers "ALL:@SECLEVEL=0";
|
||||||
ssl_prefer_server_ciphers off;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_pass http://localhost:8444/;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Pokken, comment this out if you don't plan on serving pokken.
|
|
||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
server_name pokken.hostname.here;
|
|
||||||
|
|
||||||
ssl_certificate /path/to/cert/pokken.pem;
|
|
||||||
ssl_certificate_key /path/to/cert/pokken.key;
|
|
||||||
ssl_session_timeout 1d;
|
|
||||||
ssl_session_cache shared:MozSSL:10m;
|
|
||||||
ssl_session_tickets off;
|
|
||||||
|
|
||||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
|
|
||||||
ssl_ciphers "ALL:@SECLEVEL=1";
|
|
||||||
ssl_prefer_server_ciphers off;
|
ssl_prefer_server_ciphers off;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_pass_request_headers on;
|
proxy_pass_request_headers on;
|
||||||
proxy_pass http://localhost:8080/;
|
proxy_pass http://127.0.0.1:8080/;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# CXB, comment this out if you don't plan on serving crossbeats.
|
|
||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
server_name cxb.hostname.here;
|
|
||||||
|
|
||||||
ssl_certificate /path/to/cert/cxb.pem;
|
|
||||||
ssl_certificate_key /path/to/cert/cxb.key;
|
|
||||||
ssl_session_timeout 1d;
|
|
||||||
ssl_session_cache shared:MozSSL:10m;
|
|
||||||
ssl_session_tickets off;
|
|
||||||
|
|
||||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
|
|
||||||
ssl_ciphers "ALL:@SECLEVEL=1";
|
|
||||||
ssl_prefer_server_ciphers off;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_pass_request_headers on;
|
|
||||||
proxy_pass http://localhost:8080/SDBT/104/;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,12 +73,12 @@ server {
|
|||||||
|
|
||||||
location / {
|
location / {
|
||||||
return 301 https://$host$request_uri;
|
return 301 https://$host$request_uri;
|
||||||
# If you don't want https redirection, comment the line above and uncomment the line below
|
# If you don't want https redirection, or are using something like cloudflare to manage HTTPS, comment out the line above and uncomment the line below
|
||||||
# proxy_pass http://localhost:8090/;
|
# proxy_pass http://127.0.0.1:8090/;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Frontend HTTPS. Comment out if you on't intend to use the frontend
|
# Frontend HTTPS. Comment out if you on't intend to use the frontend, or have cloudflare or something managing https for you.
|
||||||
server {
|
server {
|
||||||
listen 443 ssl;
|
listen 443 ssl;
|
||||||
server_name frontend.hostname.here;
|
server_name frontend.hostname.here;
|
||||||
@@ -141,6 +100,6 @@ server {
|
|||||||
location / {
|
location / {
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_pass_request_headers on;
|
proxy_pass_request_headers on;
|
||||||
proxy_pass http://localhost:8090/;
|
proxy_pass http://127.0.0.1:8090/;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
server:
|
server:
|
||||||
enable: True
|
enable: True
|
||||||
loglevel: "info"
|
loglevel: "info"
|
||||||
|
use_https: False
|
||||||
|
|
||||||
gachas:
|
gachas:
|
||||||
enabled_gachas:
|
enabled_gachas:
|
||||||
@@ -35,3 +36,6 @@ version:
|
|||||||
card_maker: 1.30.01
|
card_maker: 1.30.01
|
||||||
7:
|
7:
|
||||||
card_maker: 1.35.03
|
card_maker: 1.35.03
|
||||||
|
|
||||||
|
crypto:
|
||||||
|
encrypted_only: False
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
server:
|
server:
|
||||||
hostname: "localhost"
|
|
||||||
enable: True
|
enable: True
|
||||||
loglevel: "info"
|
loglevel: "info"
|
||||||
port: 9000
|
|
||||||
auto_register: True
|
auto_register: True
|
||||||
|
|
||||||
|
crypt:
|
||||||
|
enable: False
|
||||||
|
key: ""
|
||||||
|
iv: ""
|
||||||
|
|
||||||
|
hash:
|
||||||
|
verify_hash: False
|
||||||
|
hash_base: ""
|
||||||
374
index.py
374
index.py
@@ -1,319 +1,117 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import argparse
|
import argparse
|
||||||
import logging, coloredlogs
|
|
||||||
from logging.handlers import TimedRotatingFileHandler
|
|
||||||
from typing import Dict
|
|
||||||
import yaml
|
import yaml
|
||||||
from os import path, mkdir, access, W_OK
|
from os import path, environ
|
||||||
from core import *
|
import uvicorn
|
||||||
|
import logging
|
||||||
|
import asyncio
|
||||||
|
|
||||||
from twisted.web import server, resource
|
from core import CoreConfig, AimedbServlette
|
||||||
from twisted.internet import reactor, endpoints
|
|
||||||
from twisted.web.http import Request
|
|
||||||
from routes import Mapper
|
|
||||||
from threading import Thread
|
|
||||||
|
|
||||||
class HttpDispatcher(resource.Resource):
|
async def launch_main(cfg: CoreConfig, ssl: bool) -> None:
|
||||||
def __init__(self, cfg: CoreConfig, config_dir: str):
|
if ssl:
|
||||||
super().__init__()
|
server_cfg = uvicorn.Config(
|
||||||
self.config = cfg
|
"core.app:app",
|
||||||
self.isLeaf = True
|
host=cfg.server.listen_address,
|
||||||
self.map_get = Mapper()
|
port=cfg.server.port if args.port == 0 else args.port,
|
||||||
self.map_post = Mapper()
|
reload=cfg.server.is_develop,
|
||||||
self.logger = logging.getLogger("core")
|
log_level="info" if cfg.server.is_develop else "critical",
|
||||||
|
ssl_version=3,
|
||||||
self.allnet = AllnetServlet(cfg, config_dir)
|
ssl_certfile=cfg.server.ssl_cert,
|
||||||
self.title = TitleServlet(cfg, config_dir)
|
ssl_keyfile=cfg.server.ssl_key
|
||||||
self.mucha = MuchaServlet(cfg, config_dir)
|
|
||||||
|
|
||||||
self.map_get.connect(
|
|
||||||
"allnet_downloadorder_ini",
|
|
||||||
"/dl/ini/{file}",
|
|
||||||
controller="allnet",
|
|
||||||
action="handle_dlorder_ini",
|
|
||||||
conditions=dict(method=["GET"]),
|
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
self.map_post.connect(
|
server_cfg = uvicorn.Config(
|
||||||
"allnet_downloadorder_report",
|
"core.app:app",
|
||||||
"/report-api/Report",
|
host=cfg.server.listen_address,
|
||||||
controller="allnet",
|
port=cfg.server.port if args.port == 0 else args.port,
|
||||||
action="handle_dlorder_report",
|
reload=cfg.server.is_develop,
|
||||||
conditions=dict(method=["POST"]),
|
log_level="info" if cfg.server.is_develop else "critical"
|
||||||
)
|
)
|
||||||
|
server = uvicorn.Server(server_cfg)
|
||||||
|
await server.serve()
|
||||||
|
|
||||||
self.map_get.connect(
|
async def launch_billing(cfg: CoreConfig) -> None:
|
||||||
"allnet_ping",
|
server_cfg = uvicorn.Config(
|
||||||
"/naomitest.html",
|
"core.allnet:app_billing",
|
||||||
controller="allnet",
|
host=cfg.server.listen_address,
|
||||||
action="handle_naomitest",
|
port=cfg.billing.port,
|
||||||
conditions=dict(method=["GET"]),
|
reload=cfg.server.is_develop,
|
||||||
)
|
log_level="info" if cfg.server.is_develop else "critical",
|
||||||
self.map_post.connect(
|
ssl_version=3,
|
||||||
"allnet_poweron",
|
ssl_certfile=cfg.billing.ssl_cert,
|
||||||
"/sys/servlet/PowerOn",
|
ssl_keyfile=cfg.billing.ssl_key,
|
||||||
controller="allnet",
|
ssl_ciphers="DEFAULT:!aNULL:!eNULL:!MD5:!3DES:!DES:!RC4:!IDEA:!SEED:!aDSS:!SRP:!PSK",
|
||||||
action="handle_poweron",
|
)
|
||||||
conditions=dict(method=["POST"]),
|
server = uvicorn.Server(server_cfg)
|
||||||
)
|
await server.serve()
|
||||||
self.map_post.connect(
|
|
||||||
"allnet_downloadorder",
|
|
||||||
"/sys/servlet/DownloadOrder",
|
|
||||||
controller="allnet",
|
|
||||||
action="handle_dlorder",
|
|
||||||
conditions=dict(method=["POST"]),
|
|
||||||
)
|
|
||||||
self.map_post.connect(
|
|
||||||
"allnet_loaderstaterecorder",
|
|
||||||
"/sys/servlet/LoaderStateRecorder",
|
|
||||||
controller="allnet",
|
|
||||||
action="handle_loaderstaterecorder",
|
|
||||||
conditions=dict(method=["POST"]),
|
|
||||||
)
|
|
||||||
self.map_post.connect(
|
|
||||||
"allnet_alive",
|
|
||||||
"/sys/servlet/Alive",
|
|
||||||
controller="allnet",
|
|
||||||
action="handle_alive",
|
|
||||||
conditions=dict(method=["POST"]),
|
|
||||||
)
|
|
||||||
self.map_get.connect(
|
|
||||||
"allnet_alive",
|
|
||||||
"/sys/servlet/Alive",
|
|
||||||
controller="allnet",
|
|
||||||
action="handle_alive",
|
|
||||||
conditions=dict(method=["GET"]),
|
|
||||||
)
|
|
||||||
self.map_post.connect(
|
|
||||||
"allnet_billing",
|
|
||||||
"/request",
|
|
||||||
controller="allnet",
|
|
||||||
action="handle_billing_request",
|
|
||||||
conditions=dict(method=["POST"]),
|
|
||||||
)
|
|
||||||
self.map_post.connect(
|
|
||||||
"allnet_billing",
|
|
||||||
"/request/",
|
|
||||||
controller="allnet",
|
|
||||||
action="handle_billing_request",
|
|
||||||
conditions=dict(method=["POST"]),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Maintain compatability
|
async def launch_frontend(cfg: CoreConfig) -> None:
|
||||||
self.map_post.connect(
|
server_cfg = uvicorn.Config(
|
||||||
"mucha_boardauth",
|
"core.frontend:app",
|
||||||
"/mucha/boardauth.do",
|
host=cfg.server.listen_address,
|
||||||
controller="mucha",
|
port=cfg.frontend.port,
|
||||||
action="handle_boardauth",
|
reload=cfg.server.is_develop,
|
||||||
conditions=dict(method=["POST"]),
|
log_level="info" if cfg.server.is_develop else "critical",
|
||||||
)
|
)
|
||||||
self.map_post.connect(
|
server = uvicorn.Server(server_cfg)
|
||||||
"mucha_updatacheck",
|
await server.serve()
|
||||||
"/mucha/updatacheck.do",
|
|
||||||
controller="mucha",
|
|
||||||
action="handle_updatecheck",
|
|
||||||
conditions=dict(method=["POST"]),
|
|
||||||
)
|
|
||||||
self.map_post.connect(
|
|
||||||
"mucha_dlstate",
|
|
||||||
"/mucha/downloadstate.do",
|
|
||||||
controller="mucha",
|
|
||||||
action="handle_dlstate",
|
|
||||||
conditions=dict(method=["POST"]),
|
|
||||||
)
|
|
||||||
|
|
||||||
self.map_post.connect(
|
async def launch_allnet(cfg: CoreConfig) -> None:
|
||||||
"mucha_boardauth",
|
server_cfg = uvicorn.Config(
|
||||||
"/mucha_front/boardauth.do",
|
"core.allnet:app_allnet",
|
||||||
controller="mucha",
|
host=cfg.server.listen_address,
|
||||||
action="handle_boardauth",
|
port=cfg.allnet.port,
|
||||||
conditions=dict(method=["POST"]),
|
reload=cfg.server.is_develop,
|
||||||
)
|
log_level="info" if cfg.server.is_develop else "critical",
|
||||||
self.map_post.connect(
|
)
|
||||||
"mucha_updatacheck",
|
server = uvicorn.Server(server_cfg)
|
||||||
"/mucha_front/updatacheck.do",
|
await server.serve()
|
||||||
controller="mucha",
|
|
||||||
action="handle_updatecheck",
|
|
||||||
conditions=dict(method=["POST"]),
|
|
||||||
)
|
|
||||||
self.map_post.connect(
|
|
||||||
"mucha_dlstate",
|
|
||||||
"/mucha_front/downloadstate.do",
|
|
||||||
controller="mucha",
|
|
||||||
action="handle_dlstate",
|
|
||||||
conditions=dict(method=["POST"]),
|
|
||||||
)
|
|
||||||
|
|
||||||
self.map_get.connect(
|
|
||||||
"title_get",
|
|
||||||
"/{game}/{version}/{endpoint:.*?}",
|
|
||||||
controller="title",
|
|
||||||
action="render_GET",
|
|
||||||
conditions=dict(method=["GET"]),
|
|
||||||
requirements=dict(game=R"S..."),
|
|
||||||
)
|
|
||||||
self.map_post.connect(
|
|
||||||
"title_post",
|
|
||||||
"/{game}/{version}/{endpoint:.*?}",
|
|
||||||
controller="title",
|
|
||||||
action="render_POST",
|
|
||||||
conditions=dict(method=["POST"]),
|
|
||||||
requirements=dict(game=R"S..."),
|
|
||||||
)
|
|
||||||
|
|
||||||
def render_GET(self, request: Request) -> bytes:
|
async def launcher(cfg: CoreConfig, ssl: bool) -> None:
|
||||||
test = self.map_get.match(request.uri.decode())
|
task_list = [asyncio.create_task(launch_main(cfg, ssl))]
|
||||||
client_ip = Utils.get_ip_addr(request)
|
|
||||||
|
|
||||||
if test is None:
|
if cfg.billing.standalone:
|
||||||
self.logger.debug(
|
task_list.append(asyncio.create_task(launch_billing(cfg)))
|
||||||
f"Unknown GET endpoint {request.uri.decode()} from {client_ip} to port {request.getHost().port}"
|
if cfg.frontend.enable:
|
||||||
)
|
task_list.append(asyncio.create_task(launch_frontend(cfg)))
|
||||||
request.setResponseCode(404)
|
if cfg.allnet.standalone:
|
||||||
return b"Endpoint not found."
|
task_list.append(asyncio.create_task(launch_allnet(cfg)))
|
||||||
|
if cfg.aimedb.enable:
|
||||||
|
AimedbServlette(cfg).start()
|
||||||
|
|
||||||
return self.dispatch(test, request)
|
done, pending = await asyncio.wait(
|
||||||
|
task_list,
|
||||||
def render_POST(self, request: Request) -> bytes:
|
return_when=asyncio.FIRST_COMPLETED,
|
||||||
test = self.map_post.match(request.uri.decode())
|
)
|
||||||
client_ip = Utils.get_ip_addr(request)
|
|
||||||
|
|
||||||
if test is None:
|
|
||||||
self.logger.debug(
|
|
||||||
f"Unknown POST endpoint {request.uri.decode()} from {client_ip} to port {request.getHost().port}"
|
|
||||||
)
|
|
||||||
request.setResponseCode(404)
|
|
||||||
return b"Endpoint not found."
|
|
||||||
|
|
||||||
return self.dispatch(test, request)
|
|
||||||
|
|
||||||
def dispatch(self, matcher: Dict, request: Request) -> bytes:
|
|
||||||
controller = getattr(self, matcher["controller"], None)
|
|
||||||
if controller is None:
|
|
||||||
self.logger.error(
|
|
||||||
f"Controller {matcher['controller']} not found via endpoint {request.uri.decode()}"
|
|
||||||
)
|
|
||||||
request.setResponseCode(404)
|
|
||||||
return b"Endpoint not found."
|
|
||||||
|
|
||||||
handler = getattr(controller, matcher["action"], None)
|
|
||||||
if handler is None:
|
|
||||||
self.logger.error(
|
|
||||||
f"Action {matcher['action']} not found in controller {matcher['controller']} via endpoint {request.uri.decode()}"
|
|
||||||
)
|
|
||||||
request.setResponseCode(404)
|
|
||||||
return b"Endpoint not found."
|
|
||||||
|
|
||||||
url_vars = matcher
|
|
||||||
url_vars.pop("controller")
|
|
||||||
url_vars.pop("action")
|
|
||||||
ret = handler(request, url_vars)
|
|
||||||
|
|
||||||
if type(ret) == str:
|
|
||||||
return ret.encode()
|
|
||||||
|
|
||||||
elif type(ret) == bytes or type(ret) == tuple: # allow for bytes or tuple (data, response code) responses
|
|
||||||
return ret
|
|
||||||
|
|
||||||
elif ret is None:
|
|
||||||
self.logger.warning(f"None returned by controller for {request.uri.decode()} endpoint")
|
|
||||||
return b""
|
|
||||||
|
|
||||||
else:
|
|
||||||
self.logger.warning(f"Unknown data type returned by controller for {request.uri.decode()} endpoint")
|
|
||||||
return b""
|
|
||||||
|
|
||||||
|
logging.getLogger("core").info("Shutdown")
|
||||||
|
for pending_task in pending:
|
||||||
|
pending_task.cancel("Another service died, server is shutting down")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(description="ARTEMiS main entry point")
|
parser = argparse.ArgumentParser(description="Artemis main entry point")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--config", "-c", type=str, default="config", help="Configuration folder"
|
"--config", "-c", type=str, default="config", help="Configuration folder"
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--port", "-p", type=int, default=0, help="Port override"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--ssl", "-s", type=bool, help="Launch with SSL"
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if not path.exists(f"{args.config}/core.yaml"):
|
if not path.exists(f"{args.config}/core.yaml"):
|
||||||
print(
|
print(
|
||||||
f"The config folder you specified ({args.config}) does not exist or does not contain core.yaml.\nDid you copy the example folder?"
|
f"The config folder you specified ({args.config}) does not exist or does not contain core.yaml. Defaults will be used.\nDid you copy the example folder?"
|
||||||
)
|
)
|
||||||
exit(1)
|
|
||||||
|
|
||||||
cfg: CoreConfig = CoreConfig()
|
cfg: CoreConfig = CoreConfig()
|
||||||
if path.exists(f"{args.config}/core.yaml"):
|
if path.exists(f"{args.config}/core.yaml"):
|
||||||
cfg.update(yaml.safe_load(open(f"{args.config}/core.yaml")))
|
cfg.update(yaml.safe_load(open(f"{args.config}/core.yaml")))
|
||||||
|
|
||||||
if not path.exists(cfg.server.log_dir):
|
environ["ARTEMIS_CFG_DIR"] = args.config
|
||||||
mkdir(cfg.server.log_dir)
|
|
||||||
|
|
||||||
if not access(cfg.server.log_dir, W_OK):
|
asyncio.run(launcher(cfg, args.ssl))
|
||||||
print(
|
|
||||||
f"Log directory {cfg.server.log_dir} NOT writable, please check permissions"
|
|
||||||
)
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
logger = logging.getLogger("core")
|
|
||||||
log_fmt_str = "[%(asctime)s] Core | %(levelname)s | %(message)s"
|
|
||||||
log_fmt = logging.Formatter(log_fmt_str)
|
|
||||||
|
|
||||||
fileHandler = TimedRotatingFileHandler(
|
|
||||||
"{0}/{1}.log".format(cfg.server.log_dir, "core"), when="d", backupCount=10
|
|
||||||
)
|
|
||||||
fileHandler.setFormatter(log_fmt)
|
|
||||||
|
|
||||||
consoleHandler = logging.StreamHandler()
|
|
||||||
consoleHandler.setFormatter(log_fmt)
|
|
||||||
|
|
||||||
logger.addHandler(fileHandler)
|
|
||||||
logger.addHandler(consoleHandler)
|
|
||||||
|
|
||||||
log_lv = logging.DEBUG if cfg.server.is_develop else logging.INFO
|
|
||||||
logger.setLevel(log_lv)
|
|
||||||
coloredlogs.install(level=log_lv, logger=logger, fmt=log_fmt_str)
|
|
||||||
|
|
||||||
if not cfg.aimedb.key:
|
|
||||||
logger.error("!!AIMEDB KEY BLANK, SET KEY IN CORE.YAML!!")
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
f"ARTEMiS starting in {'develop' if cfg.server.is_develop else 'production'} mode"
|
|
||||||
)
|
|
||||||
|
|
||||||
allnet_server_str = f"tcp:{cfg.allnet.port}:interface={cfg.server.listen_address}"
|
|
||||||
title_server_str = f"tcp:{cfg.title.port}:interface={cfg.server.listen_address}"
|
|
||||||
adb_server_str = f"tcp:{cfg.aimedb.port}:interface={cfg.server.listen_address}"
|
|
||||||
frontend_server_str = (
|
|
||||||
f"tcp:{cfg.frontend.port}:interface={cfg.server.listen_address}"
|
|
||||||
)
|
|
||||||
|
|
||||||
billing_server_str = f"tcp:{cfg.billing.port}:interface={cfg.server.listen_address}"
|
|
||||||
if cfg.server.is_develop:
|
|
||||||
billing_server_str = (
|
|
||||||
f"ssl:{cfg.billing.port}:interface={cfg.server.listen_address}"
|
|
||||||
f":privateKey={cfg.billing.ssl_key}:certKey={cfg.billing.ssl_cert}"
|
|
||||||
)
|
|
||||||
|
|
||||||
dispatcher = HttpDispatcher(cfg, args.config)
|
|
||||||
|
|
||||||
endpoints.serverFromString(reactor, allnet_server_str).listen(
|
|
||||||
server.Site(dispatcher)
|
|
||||||
)
|
|
||||||
endpoints.serverFromString(reactor, adb_server_str).listen(AimedbFactory(cfg))
|
|
||||||
|
|
||||||
if cfg.frontend.enable:
|
|
||||||
endpoints.serverFromString(reactor, frontend_server_str).listen(
|
|
||||||
server.Site(FrontendServlet(cfg, args.config))
|
|
||||||
)
|
|
||||||
|
|
||||||
if cfg.billing.port > 0:
|
|
||||||
endpoints.serverFromString(reactor, billing_server_str).listen(
|
|
||||||
server.Site(dispatcher)
|
|
||||||
)
|
|
||||||
|
|
||||||
if cfg.title.port > 0:
|
|
||||||
endpoints.serverFromString(reactor, title_server_str).listen(
|
|
||||||
server.Site(dispatcher)
|
|
||||||
)
|
|
||||||
|
|
||||||
if cfg.server.threading:
|
|
||||||
Thread(target=reactor.run, args=(False,)).start()
|
|
||||||
else:
|
|
||||||
reactor.run()
|
|
||||||
|
|||||||
10
read.py
10
read.py
@@ -1,4 +1,4 @@
|
|||||||
# vim: set fileencoding=utf-8
|
#!/usr/bin/env python3
|
||||||
import argparse
|
import argparse
|
||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
@@ -6,6 +6,7 @@ import yaml
|
|||||||
from os import path
|
from os import path
|
||||||
import logging
|
import logging
|
||||||
import coloredlogs
|
import coloredlogs
|
||||||
|
import asyncio
|
||||||
|
|
||||||
from logging.handlers import TimedRotatingFileHandler
|
from logging.handlers import TimedRotatingFileHandler
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
@@ -39,6 +40,9 @@ class BaseReader:
|
|||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
async def read(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(description="Import Game Information")
|
parser = argparse.ArgumentParser(description="Import Game Information")
|
||||||
@@ -136,6 +140,8 @@ if __name__ == "__main__":
|
|||||||
for dir, mod in titles.items():
|
for dir, mod in titles.items():
|
||||||
if args.game in mod.game_codes:
|
if args.game in mod.game_codes:
|
||||||
handler = mod.reader(config, args.version, bin_arg, opt_arg, args.extra)
|
handler = mod.reader(config, args.version, bin_arg, opt_arg, args.extra)
|
||||||
handler.read()
|
loop = asyncio.get_event_loop()
|
||||||
|
loop.run_until_complete(handler.read())
|
||||||
|
|
||||||
|
|
||||||
logger.info("Done")
|
logger.info("Done")
|
||||||
|
|||||||
74
readme.md
74
readme.md
@@ -4,34 +4,68 @@ A network service emulator for games running SEGA'S ALL.NET service, and similar
|
|||||||
# Supported games
|
# Supported games
|
||||||
Games listed below have been tested and confirmed working. Only game versions older then the version currently active in arcades, or games versions that have not recieved a major update in over one year, are supported.
|
Games listed below have been tested and confirmed working. Only game versions older then the version currently active in arcades, or games versions that have not recieved a major update in over one year, are supported.
|
||||||
|
|
||||||
+ CHUNITHM
|
|
||||||
+ All versions up to SUN
|
|
||||||
|
|
||||||
+ crossbeats REV.
|
|
||||||
+ All versions + omnimix
|
|
||||||
|
|
||||||
+ maimai DX
|
|
||||||
+ All versions up to FESTiVAL
|
|
||||||
|
|
||||||
+ Hatsune Miku: Project DIVA Arcade
|
|
||||||
+ All versions
|
|
||||||
|
|
||||||
+ Card Maker
|
+ Card Maker
|
||||||
+ 1.30
|
+ 1.30
|
||||||
+ 1.35
|
+ 1.35
|
||||||
|
|
||||||
+ O.N.G.E.K.I.
|
+ CHUNITHM INTL
|
||||||
+ All versions up to bright MEMORY
|
+ SUPERSTAR
|
||||||
|
+ NEW
|
||||||
|
+ NEW PLUS
|
||||||
|
+ SUN
|
||||||
|
+ SUN PLUS
|
||||||
|
|
||||||
+ WACCA
|
+ CHUNITHM JP
|
||||||
+ Lily R
|
+ AIR
|
||||||
+ Reverse
|
+ AIR PLUS
|
||||||
|
+ AMAZON
|
||||||
|
+ AMAZON PLUS
|
||||||
|
+ CRYSTAL
|
||||||
|
+ CRYSTAL PLUS
|
||||||
|
+ PARADISE
|
||||||
|
+ PARADISE LOST
|
||||||
|
+ NEW
|
||||||
|
+ NEW PLUS
|
||||||
|
+ SUN
|
||||||
|
+ SUN PLUS
|
||||||
|
|
||||||
|
+ crossbeats REV.
|
||||||
|
+ Crossbeats REV.
|
||||||
|
+ Crossbeats REV. SUNRiSE S1
|
||||||
|
+ Crossbeats REV. SUNRiSE S2 + omnimix
|
||||||
|
|
||||||
|
+ Hatsune Miku: Project DIVA Arcade
|
||||||
|
+ Future Tone Arcade - All versions
|
||||||
|
|
||||||
|
+ Initial D THE ARCADE
|
||||||
|
+ Season 2
|
||||||
|
|
||||||
|
+ maimai DX
|
||||||
|
+ Splash
|
||||||
|
+ Splash Plus
|
||||||
|
+ UNiVERSE
|
||||||
|
+ UNiVERSE PLUS
|
||||||
|
+ FESTiVAL
|
||||||
|
+ FESTiVAL PLUS
|
||||||
|
+ BUDDiES
|
||||||
|
|
||||||
|
+ O.N.G.E.K.I.
|
||||||
|
+ SUMMER
|
||||||
|
+ SUMMER PLUS
|
||||||
|
+ R.E.D.
|
||||||
|
+ R.E.D. PLUS
|
||||||
|
+ bright
|
||||||
|
+ bright MEMORY
|
||||||
|
|
||||||
+ POKKÉN TOURNAMENT
|
+ POKKÉN TOURNAMENT
|
||||||
+ Final Online
|
+ Final Online
|
||||||
|
|
||||||
+ Sword Art Online Arcade (partial support)
|
+ Sword Art Online Arcade
|
||||||
+ Final
|
+ Final (Single player only)
|
||||||
|
|
||||||
|
+ WACCA
|
||||||
|
+ Lily R
|
||||||
|
+ Reverse
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
- python 3 (tested working with 3.9 and 3.10, other versions YMMV)
|
- python 3 (tested working with 3.9 and 3.10, other versions YMMV)
|
||||||
@@ -40,7 +74,7 @@ Games listed below have been tested and confirmed working. Only game versions ol
|
|||||||
- mysql/mariadb server
|
- mysql/mariadb server
|
||||||
|
|
||||||
## Setup guides
|
## Setup guides
|
||||||
Follow the platform-specific guides for [windows](docs/INSTALL_WINDOWS.md) and [ubuntu](docs/INSTALL_UBUNTU.md) to setup and run the server.
|
Follow the platform-specific guides for [windows](docs/INSTALL_WINDOWS.md), [linux (Debian 12 or Rasperry Pi OS recomended, but anything works)](docs/INSTALL_LINUX.md) or [docker](docs/INSTALL_DOCKER.md) to setup and run the server.
|
||||||
|
|
||||||
## Game specific information
|
## Game specific information
|
||||||
Read [Games specific info](docs/game_specific_info.md) for all supported games, importer settings, configuration option and database upgrades.
|
Read [Games specific info](docs/game_specific_info.md) for all supported games, importer settings, configuration option and database upgrades.
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
mypy
|
mypy
|
||||||
wheel
|
wheel
|
||||||
twisted
|
|
||||||
pytz
|
pytz
|
||||||
pyyaml
|
pyyaml
|
||||||
sqlalchemy==1.4.46
|
sqlalchemy==1.4.46
|
||||||
@@ -12,9 +11,14 @@ inflection
|
|||||||
coloredlogs
|
coloredlogs
|
||||||
pylibmc; platform_system != "Windows"
|
pylibmc; platform_system != "Windows"
|
||||||
wacky
|
wacky
|
||||||
Routes
|
|
||||||
bcrypt
|
bcrypt
|
||||||
jinja2
|
jinja2
|
||||||
protobuf
|
protobuf
|
||||||
autobahn
|
|
||||||
pillow
|
pillow
|
||||||
|
pyjwt==2.8.0
|
||||||
|
websockets
|
||||||
|
starlette
|
||||||
|
asyncio
|
||||||
|
uvicorn
|
||||||
|
alembic
|
||||||
|
python-multipart
|
||||||
5
titles/FGOA/__init__.py
Normal file
5
titles/FGOA/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from titles.fgoa.index import FGOAServlet
|
||||||
|
from titles.fgoa.const import FGOAConstants
|
||||||
|
|
||||||
|
index = FGOAServlet
|
||||||
|
game_codes = [FGOAConstants.GAME_CODE]
|
||||||
30
titles/FGOA/base.py
Normal file
30
titles/FGOA/base.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
from datetime import date, datetime, timedelta
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from core.config import CoreConfig
|
||||||
|
from titles.fgoa.config import FGOAConfig
|
||||||
|
from titles.fgoa.const import FGOAConstants
|
||||||
|
|
||||||
|
|
||||||
|
class FGOABase:
|
||||||
|
def __init__(self, core_cfg: CoreConfig, game_cfg: FGOAConfig) -> None:
|
||||||
|
self.core_cfg = core_cfg
|
||||||
|
self.game_config = game_cfg
|
||||||
|
self.date_time_format = "%Y-%m-%d %H:%M:%S"
|
||||||
|
self.date_time_format_ext = (
|
||||||
|
"%Y-%m-%d %H:%M:%S.%f" # needs to be lopped off at [:-5]
|
||||||
|
)
|
||||||
|
self.date_time_format_short = "%Y-%m-%d"
|
||||||
|
self.logger = logging.getLogger("fgoa")
|
||||||
|
self.game = FGOAConstants.GAME_CODE
|
||||||
|
self.version = FGOAConstants.VER_FGOA_SEASON_1
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_int_ver(version: str) -> str:
|
||||||
|
return version.replace(".", "")[:3]
|
||||||
|
|
||||||
|
async def handle_game_init_request(self, data: Dict) -> Dict:
|
||||||
|
return f""
|
||||||
24
titles/FGOA/config.py
Normal file
24
titles/FGOA/config.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
from core.config import CoreConfig
|
||||||
|
|
||||||
|
|
||||||
|
class FGOAServerConfig:
|
||||||
|
def __init__(self, parent: "FGOAConfig") -> None:
|
||||||
|
self.__config = parent
|
||||||
|
|
||||||
|
@property
|
||||||
|
def enable(self) -> bool:
|
||||||
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "fgo", "server", "enable", default=True
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def loglevel(self) -> int:
|
||||||
|
return CoreConfig.str_to_loglevel(
|
||||||
|
CoreConfig.get_config_field(
|
||||||
|
self.__config, "fgo", "server", "loglevel", default="info"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
class FGOAConfig(dict):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.server = FGOAServerConfig(self)
|
||||||
14
titles/FGOA/const.py
Normal file
14
titles/FGOA/const.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
class FGOAConstants():
|
||||||
|
GAME_CODE = "SDEJ"
|
||||||
|
|
||||||
|
CONFIG_NAME = "fgoa.yaml"
|
||||||
|
|
||||||
|
VER_FGOA_SEASON_1 = 0
|
||||||
|
|
||||||
|
VERSION_STRING = (
|
||||||
|
"Fate/Grand Order Arcade",
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def game_ver_to_string(cls, ver: int):
|
||||||
|
return cls.VERSION_STRING[ver]
|
||||||
123
titles/FGOA/index.py
Normal file
123
titles/FGOA/index.py
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import json
|
||||||
|
import inflection
|
||||||
|
import yaml
|
||||||
|
import string
|
||||||
|
import logging
|
||||||
|
import coloredlogs
|
||||||
|
import zlib
|
||||||
|
import base64
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
from os import path
|
||||||
|
from typing import Dict, List, Tuple
|
||||||
|
from logging.handlers import TimedRotatingFileHandler
|
||||||
|
|
||||||
|
from starlette.routing import Route
|
||||||
|
from starlette.responses import Response
|
||||||
|
from starlette.requests import Request
|
||||||
|
from starlette.responses import PlainTextResponse
|
||||||
|
|
||||||
|
from core.config import CoreConfig
|
||||||
|
from core.title import BaseServlet
|
||||||
|
from core.utils import Utils
|
||||||
|
|
||||||
|
from titles.fgoa.base import FGOABase
|
||||||
|
from titles.fgoa.config import FGOAConfig
|
||||||
|
from titles.fgoa.const import FGOAConstants
|
||||||
|
|
||||||
|
class FGOAServlet(BaseServlet):
|
||||||
|
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
|
||||||
|
self.core_cfg = core_cfg
|
||||||
|
self.game_cfg = FGOAConfig()
|
||||||
|
if path.exists(f"{cfg_dir}/{FGOAConstants.CONFIG_NAME}"):
|
||||||
|
self.game_cfg.update(
|
||||||
|
yaml.safe_load(open(f"{cfg_dir}/{FGOAConstants.CONFIG_NAME}"))
|
||||||
|
)
|
||||||
|
|
||||||
|
self.versions = [
|
||||||
|
FGOABase(core_cfg, self.game_cfg),
|
||||||
|
]
|
||||||
|
|
||||||
|
self.logger = logging.getLogger("fgoa")
|
||||||
|
log_fmt_str = "[%(asctime)s] FGOA | %(levelname)s | %(message)s"
|
||||||
|
log_fmt = logging.Formatter(log_fmt_str)
|
||||||
|
fileHandler = TimedRotatingFileHandler(
|
||||||
|
"{0}/{1}.log".format(self.core_cfg.server.log_dir, "fgoa"),
|
||||||
|
encoding="utf8",
|
||||||
|
when="d",
|
||||||
|
backupCount=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
fileHandler.setFormatter(log_fmt)
|
||||||
|
|
||||||
|
consoleHandler = logging.StreamHandler()
|
||||||
|
consoleHandler.setFormatter(log_fmt)
|
||||||
|
|
||||||
|
self.logger.addHandler(fileHandler)
|
||||||
|
self.logger.addHandler(consoleHandler)
|
||||||
|
|
||||||
|
self.logger.setLevel(self.game_cfg.server.loglevel)
|
||||||
|
coloredlogs.install(
|
||||||
|
level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_game_enabled(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> bool:
|
||||||
|
game_cfg = FGOAConfig()
|
||||||
|
|
||||||
|
if path.exists(f"{cfg_dir}/{FGOAConstants.CONFIG_NAME}"):
|
||||||
|
game_cfg.update(
|
||||||
|
yaml.safe_load(open(f"{cfg_dir}/{FGOAConstants.CONFIG_NAME}"))
|
||||||
|
)
|
||||||
|
|
||||||
|
if not game_cfg.server.enable:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_routes(self) -> List[Route]:
|
||||||
|
return [
|
||||||
|
Route("/SDEJ/{version:int}/{endpoint:str}", self.render_POST, methods=['POST'])
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]:
|
||||||
|
if not self.core_cfg.server.is_using_proxy and Utils.get_title_port(self.core_cfg) != 80:
|
||||||
|
return (f"http://{self.core_cfg.server.hostname}:{Utils.get_title_port(self.core_cfg)}/{game_code}/{game_ver}", self.core_cfg.server.hostname)
|
||||||
|
|
||||||
|
return (f"http://{self.core_cfg.server.hostname}/{game_code}/{game_ver}", self.core_cfg.server.hostname)
|
||||||
|
|
||||||
|
|
||||||
|
async def render_POST(self, request: Request) -> bytes:
|
||||||
|
version: int = request.path_params.get('version')
|
||||||
|
endpoint: str = request.path_params.get('endpoint')
|
||||||
|
req_raw = await request.body()
|
||||||
|
internal_ver = 0
|
||||||
|
client_ip = Utils.get_ip_addr(request)
|
||||||
|
|
||||||
|
if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32:
|
||||||
|
# If we get a 32 character long hex string, it's a hash and we're
|
||||||
|
# doing encrypted. The likelyhood of false positives is low but
|
||||||
|
# technically not 0
|
||||||
|
self.logger.error("Encryption not supported at this time")
|
||||||
|
|
||||||
|
self.logger.debug(req_raw)
|
||||||
|
|
||||||
|
self.logger.info(f"v{version} {endpoint} request from {client_ip}")
|
||||||
|
|
||||||
|
func_to_find = "handle_" + inflection.underscore(endpoint) + "_request"
|
||||||
|
|
||||||
|
try:
|
||||||
|
handler = getattr(self.versions[internal_ver], func_to_find)
|
||||||
|
resp = await handler(req_raw)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error handling v{version} method {endpoint} - {e}")
|
||||||
|
raise
|
||||||
|
return Response(zlib.compress(b'{"stat": "0"}'))
|
||||||
|
|
||||||
|
if resp is None:
|
||||||
|
resp = {"returnCode": 1}
|
||||||
|
|
||||||
|
self.logger.debug(f"Response {resp}")
|
||||||
|
|
||||||
|
return Response(zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")))
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
from titles.chuni.index import ChuniServlet
|
from .index import ChuniServlet
|
||||||
from titles.chuni.const import ChuniConstants
|
from .const import ChuniConstants
|
||||||
from titles.chuni.database import ChuniData
|
from .database import ChuniData
|
||||||
from titles.chuni.read import ChuniReader
|
from .read import ChuniReader
|
||||||
|
from .frontend import ChuniFrontend
|
||||||
|
|
||||||
index = ChuniServlet
|
index = ChuniServlet
|
||||||
database = ChuniData
|
database = ChuniData
|
||||||
reader = ChuniReader
|
reader = ChuniReader
|
||||||
game_codes = [ChuniConstants.GAME_CODE, ChuniConstants.GAME_CODE_NEW]
|
frontend = ChuniFrontend
|
||||||
current_schema_version = 4
|
game_codes = [ChuniConstants.GAME_CODE, ChuniConstants.GAME_CODE_NEW, ChuniConstants.GAME_CODE_INT]
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class ChuniAir(ChuniBase):
|
|||||||
super().__init__(core_cfg, game_cfg)
|
super().__init__(core_cfg, game_cfg)
|
||||||
self.version = ChuniConstants.VER_CHUNITHM_AIR
|
self.version = ChuniConstants.VER_CHUNITHM_AIR
|
||||||
|
|
||||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
async def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||||
ret = super().handle_get_game_setting_api_request(data)
|
ret = await super().handle_get_game_setting_api_request(data)
|
||||||
ret["gameSetting"]["dataVersion"] = "1.10.00"
|
ret["gameSetting"]["dataVersion"] = "1.10.00"
|
||||||
return ret
|
return ret
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class ChuniAirPlus(ChuniBase):
|
|||||||
super().__init__(core_cfg, game_cfg)
|
super().__init__(core_cfg, game_cfg)
|
||||||
self.version = ChuniConstants.VER_CHUNITHM_AIR_PLUS
|
self.version = ChuniConstants.VER_CHUNITHM_AIR_PLUS
|
||||||
|
|
||||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
async def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||||
ret = super().handle_get_game_setting_api_request(data)
|
ret = await super().handle_get_game_setting_api_request(data)
|
||||||
ret["gameSetting"]["dataVersion"] = "1.15.00"
|
ret["gameSetting"]["dataVersion"] = "1.15.00"
|
||||||
return ret
|
return ret
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class ChuniAmazon(ChuniBase):
|
|||||||
super().__init__(core_cfg, game_cfg)
|
super().__init__(core_cfg, game_cfg)
|
||||||
self.version = ChuniConstants.VER_CHUNITHM_AMAZON
|
self.version = ChuniConstants.VER_CHUNITHM_AMAZON
|
||||||
|
|
||||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
async def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||||
ret = super().handle_get_game_setting_api_request(data)
|
ret = await super().handle_get_game_setting_api_request(data)
|
||||||
ret["gameSetting"]["dataVersion"] = "1.30.00"
|
ret["gameSetting"]["dataVersion"] = "1.30.00"
|
||||||
return ret
|
return ret
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class ChuniAmazonPlus(ChuniBase):
|
|||||||
super().__init__(core_cfg, game_cfg)
|
super().__init__(core_cfg, game_cfg)
|
||||||
self.version = ChuniConstants.VER_CHUNITHM_AMAZON_PLUS
|
self.version = ChuniConstants.VER_CHUNITHM_AMAZON_PLUS
|
||||||
|
|
||||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
async def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||||
ret = super().handle_get_game_setting_api_request(data)
|
ret = await super().handle_get_game_setting_api_request(data)
|
||||||
ret["gameSetting"]["dataVersion"] = "1.35.00"
|
ret["gameSetting"]["dataVersion"] = "1.35.00"
|
||||||
return ret
|
return ret
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from core.config import CoreConfig
|
|||||||
from titles.chuni.const import ChuniConstants
|
from titles.chuni.const import ChuniConstants
|
||||||
from titles.chuni.database import ChuniData
|
from titles.chuni.database import ChuniData
|
||||||
from titles.chuni.config import ChuniConfig
|
from titles.chuni.config import ChuniConfig
|
||||||
|
SCORE_BUFFER = {}
|
||||||
|
|
||||||
class ChuniBase:
|
class ChuniBase:
|
||||||
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
|
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
|
||||||
@@ -22,7 +22,7 @@ class ChuniBase:
|
|||||||
self.game = ChuniConstants.GAME_CODE
|
self.game = ChuniConstants.GAME_CODE
|
||||||
self.version = ChuniConstants.VER_CHUNITHM
|
self.version = ChuniConstants.VER_CHUNITHM
|
||||||
|
|
||||||
def handle_game_login_api_request(self, data: Dict) -> Dict:
|
async def handle_game_login_api_request(self, data: Dict) -> Dict:
|
||||||
"""
|
"""
|
||||||
Handles the login bonus logic, required for the game because
|
Handles the login bonus logic, required for the game because
|
||||||
getUserLoginBonus gets called after getUserItem and therefore the
|
getUserLoginBonus gets called after getUserItem and therefore the
|
||||||
@@ -38,20 +38,20 @@ class ChuniBase:
|
|||||||
return {"returnCode": 1}
|
return {"returnCode": 1}
|
||||||
|
|
||||||
user_id = data["userId"]
|
user_id = data["userId"]
|
||||||
login_bonus_presets = self.data.static.get_login_bonus_presets(self.version)
|
login_bonus_presets = await self.data.static.get_login_bonus_presets(self.version)
|
||||||
|
|
||||||
for preset in login_bonus_presets:
|
for preset in login_bonus_presets:
|
||||||
# check if a user already has some pogress and if not add the
|
# check if a user already has some pogress and if not add the
|
||||||
# login bonus entry
|
# login bonus entry
|
||||||
user_login_bonus = self.data.item.get_login_bonus(
|
user_login_bonus = await self.data.item.get_login_bonus(
|
||||||
user_id, self.version, preset["presetId"]
|
user_id, self.version, preset["presetId"]
|
||||||
)
|
)
|
||||||
if user_login_bonus is None:
|
if user_login_bonus is None:
|
||||||
self.data.item.put_login_bonus(
|
await self.data.item.put_login_bonus(
|
||||||
user_id, self.version, preset["presetId"]
|
user_id, self.version, preset["presetId"]
|
||||||
)
|
)
|
||||||
# yeah i'm lazy
|
# yeah i'm lazy
|
||||||
user_login_bonus = self.data.item.get_login_bonus(
|
user_login_bonus = await self.data.item.get_login_bonus(
|
||||||
user_id, self.version, preset["presetId"]
|
user_id, self.version, preset["presetId"]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ class ChuniBase:
|
|||||||
bonus_count = user_login_bonus["bonusCount"] + 1
|
bonus_count = user_login_bonus["bonusCount"] + 1
|
||||||
last_update_date = datetime.now()
|
last_update_date = datetime.now()
|
||||||
|
|
||||||
all_login_boni = self.data.static.get_login_bonus(
|
all_login_boni = await self.data.static.get_login_bonus(
|
||||||
self.version, preset["presetId"]
|
self.version, preset["presetId"]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -91,13 +91,13 @@ class ChuniBase:
|
|||||||
is_finished = True
|
is_finished = True
|
||||||
|
|
||||||
# grab the item for the corresponding day
|
# grab the item for the corresponding day
|
||||||
login_item = self.data.static.get_login_bonus_by_required_days(
|
login_item = await self.data.static.get_login_bonus_by_required_days(
|
||||||
self.version, preset["presetId"], bonus_count
|
self.version, preset["presetId"], bonus_count
|
||||||
)
|
)
|
||||||
if login_item is not None:
|
if login_item is not None:
|
||||||
# now add the present to the database so the
|
# now add the present to the database so the
|
||||||
# handle_get_user_item_api_request can grab them
|
# handle_get_user_item_api_request can grab them
|
||||||
self.data.item.put_item(
|
await self.data.item.put_item(
|
||||||
user_id,
|
user_id,
|
||||||
{
|
{
|
||||||
"itemId": login_item["presentId"],
|
"itemId": login_item["presentId"],
|
||||||
@@ -107,7 +107,7 @@ class ChuniBase:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
self.data.item.put_login_bonus(
|
await self.data.item.put_login_bonus(
|
||||||
user_id,
|
user_id,
|
||||||
self.version,
|
self.version,
|
||||||
preset["presetId"],
|
preset["presetId"],
|
||||||
@@ -119,12 +119,12 @@ class ChuniBase:
|
|||||||
|
|
||||||
return {"returnCode": 1}
|
return {"returnCode": 1}
|
||||||
|
|
||||||
def handle_game_logout_api_request(self, data: Dict) -> Dict:
|
async def handle_game_logout_api_request(self, data: Dict) -> Dict:
|
||||||
# self.data.base.log_event("chuni", "logout", logging.INFO, {"version": self.version, "user": data["userId"]})
|
# self.data.base.log_event("chuni", "logout", logging.INFO, {"version": self.version, "user": data["userId"]})
|
||||||
return {"returnCode": 1}
|
return {"returnCode": 1}
|
||||||
|
|
||||||
def handle_get_game_charge_api_request(self, data: Dict) -> Dict:
|
async def handle_get_game_charge_api_request(self, data: Dict) -> Dict:
|
||||||
game_charge_list = self.data.static.get_enabled_charges(self.version)
|
game_charge_list = await self.data.static.get_enabled_charges(self.version)
|
||||||
|
|
||||||
if game_charge_list is None or len(game_charge_list) == 0:
|
if game_charge_list is None or len(game_charge_list) == 0:
|
||||||
return {"length": 0, "gameChargeList": []}
|
return {"length": 0, "gameChargeList": []}
|
||||||
@@ -145,8 +145,8 @@ class ChuniBase:
|
|||||||
)
|
)
|
||||||
return {"length": len(charges), "gameChargeList": charges}
|
return {"length": len(charges), "gameChargeList": charges}
|
||||||
|
|
||||||
def handle_get_game_event_api_request(self, data: Dict) -> Dict:
|
async def handle_get_game_event_api_request(self, data: Dict) -> Dict:
|
||||||
game_events = self.data.static.get_enabled_events(self.version)
|
game_events = await self.data.static.get_enabled_events(self.version)
|
||||||
|
|
||||||
if game_events is None or len(game_events) == 0:
|
if game_events is None or len(game_events) == 0:
|
||||||
self.logger.warning("No enabled events, did you run the reader?")
|
self.logger.warning("No enabled events, did you run the reader?")
|
||||||
@@ -177,25 +177,54 @@ class ChuniBase:
|
|||||||
"gameEventList": event_list,
|
"gameEventList": event_list,
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_game_idlist_api_request(self, data: Dict) -> Dict:
|
async def handle_get_game_idlist_api_request(self, data: Dict) -> Dict:
|
||||||
return {"type": data["type"], "length": 0, "gameIdlistList": []}
|
return {"type": data["type"], "length": 0, "gameIdlistList": []}
|
||||||
|
|
||||||
def handle_get_game_message_api_request(self, data: Dict) -> Dict:
|
async def handle_get_game_message_api_request(self, data: Dict) -> Dict:
|
||||||
return {"type": data["type"], "length": "0", "gameMessageList": []}
|
return {
|
||||||
|
"type": data["type"],
|
||||||
|
"length": 1,
|
||||||
|
"gameMessageList": [{
|
||||||
|
"id": 1,
|
||||||
|
"type": 1,
|
||||||
|
"message": f"Welcome to {self.core_cfg.server.name} network!" if not self.game_cfg.server.news_msg else self.game_cfg.server.news_msg,
|
||||||
|
"startDate": "2017-12-05 07:00:00.0",
|
||||||
|
"endDate": "2099-12-31 00:00:00.0"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
def handle_get_game_ranking_api_request(self, data: Dict) -> Dict:
|
async def handle_get_game_ranking_api_request(self, data: Dict) -> Dict:
|
||||||
return {"type": data["type"], "gameRankingList": []}
|
rankings = await self.data.score.get_rankings(self.version)
|
||||||
|
return {"type": data["type"], "gameRankingList": rankings}
|
||||||
|
|
||||||
def handle_get_game_sale_api_request(self, data: Dict) -> Dict:
|
async def handle_get_game_sale_api_request(self, data: Dict) -> Dict:
|
||||||
return {"type": data["type"], "length": 0, "gameSaleList": []}
|
return {"type": data["type"], "length": 0, "gameSaleList": []}
|
||||||
|
|
||||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
async def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||||
reboot_start = datetime.strftime(
|
# if reboot start/end time is not defined use the default behavior of being a few hours ago
|
||||||
datetime.now() - timedelta(hours=4), self.date_time_format
|
if self.core_cfg.title.reboot_start_time == "" or self.core_cfg.title.reboot_end_time == "":
|
||||||
)
|
reboot_start = datetime.strftime(
|
||||||
reboot_end = datetime.strftime(
|
datetime.utcnow() + timedelta(hours=6), self.date_time_format
|
||||||
datetime.now() - timedelta(hours=3), self.date_time_format
|
)
|
||||||
)
|
reboot_end = datetime.strftime(
|
||||||
|
datetime.utcnow() + timedelta(hours=7), self.date_time_format
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# get current datetime in JST
|
||||||
|
current_jst = datetime.now(pytz.timezone('Asia/Tokyo')).date()
|
||||||
|
|
||||||
|
# parse config start/end times into datetime
|
||||||
|
reboot_start_time = datetime.strptime(self.core_cfg.title.reboot_start_time, "%H:%M")
|
||||||
|
reboot_end_time = datetime.strptime(self.core_cfg.title.reboot_end_time, "%H:%M")
|
||||||
|
|
||||||
|
# offset datetimes with current date/time
|
||||||
|
reboot_start_time = reboot_start_time.replace(year=current_jst.year, month=current_jst.month, day=current_jst.day, tzinfo=pytz.timezone('Asia/Tokyo'))
|
||||||
|
reboot_end_time = reboot_end_time.replace(year=current_jst.year, month=current_jst.month, day=current_jst.day, tzinfo=pytz.timezone('Asia/Tokyo'))
|
||||||
|
|
||||||
|
# create strings for use in gameSetting
|
||||||
|
reboot_start = reboot_start_time.strftime(self.date_time_format)
|
||||||
|
reboot_end = reboot_end_time.strftime(self.date_time_format)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"gameSetting": {
|
"gameSetting": {
|
||||||
"dataVersion": "1.00.00",
|
"dataVersion": "1.00.00",
|
||||||
@@ -211,9 +240,8 @@ class ChuniBase:
|
|||||||
"isDumpUpload": "false",
|
"isDumpUpload": "false",
|
||||||
"isAou": "false",
|
"isAou": "false",
|
||||||
}
|
}
|
||||||
|
async def handle_get_user_activity_api_request(self, data: Dict) -> Dict:
|
||||||
def handle_get_user_activity_api_request(self, data: Dict) -> Dict:
|
user_activity_list = await self.data.profile.get_profile_activity(
|
||||||
user_activity_list = self.data.profile.get_profile_activity(
|
|
||||||
data["userId"], data["kind"]
|
data["userId"], data["kind"]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -233,8 +261,8 @@ class ChuniBase:
|
|||||||
"userActivityList": activity_list,
|
"userActivityList": activity_list,
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_character_api_request(self, data: Dict) -> Dict:
|
async def handle_get_user_character_api_request(self, data: Dict) -> Dict:
|
||||||
characters = self.data.item.get_characters(data["userId"])
|
characters = await self.data.item.get_characters(data["userId"])
|
||||||
if characters is None:
|
if characters is None:
|
||||||
return {
|
return {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
@@ -268,8 +296,8 @@ class ChuniBase:
|
|||||||
"userCharacterList": character_list,
|
"userCharacterList": character_list,
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_charge_api_request(self, data: Dict) -> Dict:
|
async def handle_get_user_charge_api_request(self, data: Dict) -> Dict:
|
||||||
user_charge_list = self.data.profile.get_profile_charge(data["userId"])
|
user_charge_list = await self.data.profile.get_profile_charge(data["userId"])
|
||||||
|
|
||||||
charge_list = []
|
charge_list = []
|
||||||
for charge in user_charge_list:
|
for charge in user_charge_list:
|
||||||
@@ -284,8 +312,15 @@ class ChuniBase:
|
|||||||
"userChargeList": charge_list,
|
"userChargeList": charge_list,
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_course_api_request(self, data: Dict) -> Dict:
|
async def handle_get_user_recent_player_api_request(self, data: Dict) -> Dict:
|
||||||
user_course_list = self.data.score.get_courses(data["userId"])
|
return {
|
||||||
|
"userId": data["userId"],
|
||||||
|
"length": 0,
|
||||||
|
"userRecentPlayerList": [], # playUserId, playUserName, playDate, friendPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
async def handle_get_user_course_api_request(self, data: Dict) -> Dict:
|
||||||
|
user_course_list = await self.data.score.get_courses(data["userId"])
|
||||||
if user_course_list is None:
|
if user_course_list is None:
|
||||||
return {
|
return {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
@@ -295,8 +330,8 @@ class ChuniBase:
|
|||||||
}
|
}
|
||||||
|
|
||||||
course_list = []
|
course_list = []
|
||||||
next_idx = int(data["nextIndex"])
|
next_idx = int(data.get("nextIndex", 0))
|
||||||
max_ct = int(data["maxCount"])
|
max_ct = int(data.get("maxCount", 300))
|
||||||
|
|
||||||
for x in range(next_idx, len(user_course_list)):
|
for x in range(next_idx, len(user_course_list)):
|
||||||
tmp = user_course_list[x]._asdict()
|
tmp = user_course_list[x]._asdict()
|
||||||
@@ -319,8 +354,8 @@ class ChuniBase:
|
|||||||
"userCourseList": course_list,
|
"userCourseList": course_list,
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_data_api_request(self, data: Dict) -> Dict:
|
async def handle_get_user_data_api_request(self, data: Dict) -> Dict:
|
||||||
p = self.data.profile.get_profile_data(data["userId"], self.version)
|
p = await self.data.profile.get_profile_data(data["userId"], self.version)
|
||||||
if p is None:
|
if p is None:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@@ -331,8 +366,8 @@ class ChuniBase:
|
|||||||
|
|
||||||
return {"userId": data["userId"], "userData": profile}
|
return {"userId": data["userId"], "userData": profile}
|
||||||
|
|
||||||
def handle_get_user_data_ex_api_request(self, data: Dict) -> Dict:
|
async def handle_get_user_data_ex_api_request(self, data: Dict) -> Dict:
|
||||||
p = self.data.profile.get_profile_data_ex(data["userId"], self.version)
|
p = await self.data.profile.get_profile_data_ex(data["userId"], self.version)
|
||||||
if p is None:
|
if p is None:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@@ -343,8 +378,8 @@ class ChuniBase:
|
|||||||
|
|
||||||
return {"userId": data["userId"], "userDataEx": profile}
|
return {"userId": data["userId"], "userDataEx": profile}
|
||||||
|
|
||||||
def handle_get_user_duel_api_request(self, data: Dict) -> Dict:
|
async def handle_get_user_duel_api_request(self, data: Dict) -> Dict:
|
||||||
user_duel_list = self.data.item.get_duels(data["userId"])
|
user_duel_list = await self.data.item.get_duels(data["userId"])
|
||||||
if user_duel_list is None:
|
if user_duel_list is None:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@@ -361,12 +396,78 @@ class ChuniBase:
|
|||||||
"userDuelList": duel_list,
|
"userDuelList": duel_list,
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict:
|
async def handle_get_user_rival_data_api_request(self, data: Dict) -> Dict:
|
||||||
|
p = await self.data.profile.get_rival(data["rivalId"])
|
||||||
|
if p is None:
|
||||||
|
return {}
|
||||||
|
userRivalData = {
|
||||||
|
"rivalId": p.user,
|
||||||
|
"rivalName": p.userName
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"userId": data["userId"],
|
||||||
|
"userRivalData": userRivalData
|
||||||
|
}
|
||||||
|
|
||||||
|
async def handle_get_user_rival_music_api_request(self, data: Dict) -> Dict:
|
||||||
|
rival_id = data["rivalId"]
|
||||||
|
next_index = int(data["nextIndex"])
|
||||||
|
max_count = int(data["maxCount"])
|
||||||
|
user_rival_music_list = []
|
||||||
|
|
||||||
|
# Fetch all the rival music entries for the user
|
||||||
|
all_entries = await self.data.score.get_rival_music(rival_id)
|
||||||
|
|
||||||
|
# Process the entries based on max_count and nextIndex
|
||||||
|
for music in all_entries:
|
||||||
|
music_id = music["musicId"]
|
||||||
|
level = music["level"]
|
||||||
|
score = music["scoreMax"]
|
||||||
|
rank = music["scoreRank"]
|
||||||
|
|
||||||
|
# Create a music entry for the current music_id if it's unique
|
||||||
|
music_entry = next((entry for entry in user_rival_music_list if entry["musicId"] == music_id), None)
|
||||||
|
if music_entry is None:
|
||||||
|
music_entry = {
|
||||||
|
"musicId": music_id,
|
||||||
|
"length": 0,
|
||||||
|
"userRivalMusicDetailList": []
|
||||||
|
}
|
||||||
|
user_rival_music_list.append(music_entry)
|
||||||
|
|
||||||
|
# Create a level entry for the current level if it's unique or has a higher score
|
||||||
|
level_entry = next((entry for entry in music_entry["userRivalMusicDetailList"] if entry["level"] == level), None)
|
||||||
|
if level_entry is None:
|
||||||
|
level_entry = {
|
||||||
|
"level": level,
|
||||||
|
"scoreMax": score,
|
||||||
|
"scoreRank": rank
|
||||||
|
}
|
||||||
|
music_entry["userRivalMusicDetailList"].append(level_entry)
|
||||||
|
elif score > level_entry["scoreMax"]:
|
||||||
|
level_entry["scoreMax"] = score
|
||||||
|
level_entry["scoreRank"] = rank
|
||||||
|
|
||||||
|
# Calculate the length for each "musicId" by counting the unique levels
|
||||||
|
for music_entry in user_rival_music_list:
|
||||||
|
music_entry["length"] = len(music_entry["userRivalMusicDetailList"])
|
||||||
|
|
||||||
|
# Prepare the result dictionary with user rival music data
|
||||||
|
result = {
|
||||||
|
"userId": data["userId"],
|
||||||
|
"rivalId": data["rivalId"],
|
||||||
|
"nextIndex": str(next_index + len(user_rival_music_list[next_index: next_index + max_count]) if max_count <= len(user_rival_music_list[next_index: next_index + max_count]) else -1),
|
||||||
|
"userRivalMusicList": user_rival_music_list[next_index: next_index + max_count]
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict:
|
||||||
user_fav_item_list = []
|
user_fav_item_list = []
|
||||||
|
|
||||||
# still needs to be implemented on WebUI
|
# still needs to be implemented on WebUI
|
||||||
# 1: Music, 3: Character
|
# 1: Music, 2: User, 3: Character
|
||||||
fav_list = self.data.item.get_all_favorites(
|
fav_list = await self.data.item.get_all_favorites(
|
||||||
data["userId"], self.version, fav_kind=int(data["kind"])
|
data["userId"], self.version, fav_kind=int(data["kind"])
|
||||||
)
|
)
|
||||||
if fav_list is not None:
|
if fav_list is not None:
|
||||||
@@ -381,17 +482,17 @@ class ChuniBase:
|
|||||||
"userFavoriteItemList": user_fav_item_list,
|
"userFavoriteItemList": user_fav_item_list,
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_favorite_music_api_request(self, data: Dict) -> Dict:
|
async def handle_get_user_favorite_music_api_request(self, data: Dict) -> Dict:
|
||||||
"""
|
"""
|
||||||
This is handled via the webui, which we don't have right now
|
This is handled via the webui, which we don't have right now
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return {"userId": data["userId"], "length": 0, "userFavoriteMusicList": []}
|
return {"userId": data["userId"], "length": 0, "userFavoriteMusicList": []}
|
||||||
|
|
||||||
def handle_get_user_item_api_request(self, data: Dict) -> Dict:
|
async def handle_get_user_item_api_request(self, data: Dict) -> Dict:
|
||||||
kind = int(int(data["nextIndex"]) / 10000000000)
|
kind = int(int(data["nextIndex"]) / 10000000000)
|
||||||
next_idx = int(int(data["nextIndex"]) % 10000000000)
|
next_idx = int(int(data["nextIndex"]) % 10000000000)
|
||||||
user_item_list = self.data.item.get_items(data["userId"], kind)
|
user_item_list = await self.data.item.get_items(data["userId"], kind)
|
||||||
|
|
||||||
if user_item_list is None or len(user_item_list) == 0:
|
if user_item_list is None or len(user_item_list) == 0:
|
||||||
return {
|
return {
|
||||||
@@ -425,9 +526,9 @@ class ChuniBase:
|
|||||||
"userItemList": items,
|
"userItemList": items,
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict:
|
async def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict:
|
||||||
user_id = data["userId"]
|
user_id = data["userId"]
|
||||||
user_login_bonus = self.data.item.get_all_login_bonus(user_id, self.version)
|
user_login_bonus = await self.data.item.get_all_login_bonus(user_id, self.version)
|
||||||
# ignore the loginBonus request if its disabled in config
|
# ignore the loginBonus request if its disabled in config
|
||||||
if user_login_bonus is None or not self.game_cfg.mods.use_login_bonus:
|
if user_login_bonus is None or not self.game_cfg.mods.use_login_bonus:
|
||||||
return {"userId": user_id, "length": 0, "userLoginBonusList": []}
|
return {"userId": user_id, "length": 0, "userLoginBonusList": []}
|
||||||
@@ -451,8 +552,8 @@ class ChuniBase:
|
|||||||
"userLoginBonusList": user_login_list,
|
"userLoginBonusList": user_login_list,
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_map_api_request(self, data: Dict) -> Dict:
|
async def handle_get_user_map_api_request(self, data: Dict) -> Dict:
|
||||||
user_map_list = self.data.item.get_maps(data["userId"])
|
user_map_list = await self.data.item.get_maps(data["userId"])
|
||||||
if user_map_list is None:
|
if user_map_list is None:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@@ -469,8 +570,8 @@ class ChuniBase:
|
|||||||
"userMapList": map_list,
|
"userMapList": map_list,
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_music_api_request(self, data: Dict) -> Dict:
|
async def handle_get_user_music_api_request(self, data: Dict) -> Dict:
|
||||||
music_detail = self.data.score.get_scores(data["userId"])
|
music_detail = await self.data.score.get_scores(data["userId"])
|
||||||
if music_detail is None:
|
if music_detail is None:
|
||||||
return {
|
return {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
@@ -490,22 +591,37 @@ class ChuniBase:
|
|||||||
tmp.pop("id")
|
tmp.pop("id")
|
||||||
|
|
||||||
for song in song_list:
|
for song in song_list:
|
||||||
|
score_buf = SCORE_BUFFER.get(str(data["userId"])) or []
|
||||||
if song["userMusicDetailList"][0]["musicId"] == tmp["musicId"]:
|
if song["userMusicDetailList"][0]["musicId"] == tmp["musicId"]:
|
||||||
found = True
|
found = True
|
||||||
song["userMusicDetailList"].append(tmp)
|
song["userMusicDetailList"].append(tmp)
|
||||||
song["length"] = len(song["userMusicDetailList"])
|
song["length"] = len(song["userMusicDetailList"])
|
||||||
|
score_buf.append(tmp["musicId"])
|
||||||
|
SCORE_BUFFER[str(data["userId"])] = score_buf
|
||||||
|
|
||||||
if not found:
|
score_buf = SCORE_BUFFER.get(str(data["userId"])) or []
|
||||||
|
if not found and tmp["musicId"] not in score_buf:
|
||||||
song_list.append({"length": 1, "userMusicDetailList": [tmp]})
|
song_list.append({"length": 1, "userMusicDetailList": [tmp]})
|
||||||
|
score_buf.append(tmp["musicId"])
|
||||||
|
SCORE_BUFFER[str(data["userId"])] = score_buf
|
||||||
|
|
||||||
if len(song_list) >= max_ct:
|
if len(song_list) >= max_ct:
|
||||||
break
|
break
|
||||||
|
|
||||||
if len(song_list) >= next_idx + max_ct:
|
for songIdx in range(len(song_list)):
|
||||||
next_idx += max_ct
|
for recordIdx in range(x+1, len(music_detail)):
|
||||||
|
if song_list[songIdx]["userMusicDetailList"][0]["musicId"] == music_detail[recordIdx]["musicId"]:
|
||||||
|
music = music_detail[recordIdx]._asdict()
|
||||||
|
music.pop("user")
|
||||||
|
music.pop("id")
|
||||||
|
song_list[songIdx]["userMusicDetailList"].append(music)
|
||||||
|
song_list[songIdx]["length"] += 1
|
||||||
|
|
||||||
|
if len(song_list) >= max_ct:
|
||||||
|
next_idx += len(song_list)
|
||||||
else:
|
else:
|
||||||
next_idx = -1
|
next_idx = -1
|
||||||
|
SCORE_BUFFER[str(data["userId"])] = []
|
||||||
return {
|
return {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
"length": len(song_list),
|
"length": len(song_list),
|
||||||
@@ -513,8 +629,8 @@ class ChuniBase:
|
|||||||
"userMusicList": song_list, # 240
|
"userMusicList": song_list, # 240
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_option_api_request(self, data: Dict) -> Dict:
|
async def handle_get_user_option_api_request(self, data: Dict) -> Dict:
|
||||||
p = self.data.profile.get_profile_option(data["userId"])
|
p = await self.data.profile.get_profile_option(data["userId"])
|
||||||
|
|
||||||
option = p._asdict()
|
option = p._asdict()
|
||||||
option.pop("id")
|
option.pop("id")
|
||||||
@@ -522,8 +638,8 @@ class ChuniBase:
|
|||||||
|
|
||||||
return {"userId": data["userId"], "userGameOption": option}
|
return {"userId": data["userId"], "userGameOption": option}
|
||||||
|
|
||||||
def handle_get_user_option_ex_api_request(self, data: Dict) -> Dict:
|
async def handle_get_user_option_ex_api_request(self, data: Dict) -> Dict:
|
||||||
p = self.data.profile.get_profile_option_ex(data["userId"])
|
p = await self.data.profile.get_profile_option_ex(data["userId"])
|
||||||
|
|
||||||
option = p._asdict()
|
option = p._asdict()
|
||||||
option.pop("id")
|
option.pop("id")
|
||||||
@@ -534,11 +650,11 @@ class ChuniBase:
|
|||||||
def read_wtf8(self, src):
|
def read_wtf8(self, src):
|
||||||
return bytes([ord(c) for c in src]).decode("utf-8")
|
return bytes([ord(c) for c in src]).decode("utf-8")
|
||||||
|
|
||||||
def handle_get_user_preview_api_request(self, data: Dict) -> Dict:
|
async def handle_get_user_preview_api_request(self, data: Dict) -> Dict:
|
||||||
profile = self.data.profile.get_profile_preview(data["userId"], self.version)
|
profile = await self.data.profile.get_profile_preview(data["userId"], self.version)
|
||||||
if profile is None:
|
if profile is None:
|
||||||
return None
|
return None
|
||||||
profile_character = self.data.item.get_character(
|
profile_character = await self.data.item.get_character(
|
||||||
data["userId"], profile["characterId"]
|
data["userId"], profile["characterId"]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -576,8 +692,8 @@ class ChuniBase:
|
|||||||
"userNameEx": profile["userName"],
|
"userNameEx": profile["userName"],
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_recent_rating_api_request(self, data: Dict) -> Dict:
|
async def handle_get_user_recent_rating_api_request(self, data: Dict) -> Dict:
|
||||||
recent_rating_list = self.data.profile.get_profile_recent_rating(data["userId"])
|
recent_rating_list = await self.data.profile.get_profile_recent_rating(data["userId"])
|
||||||
if recent_rating_list is None:
|
if recent_rating_list is None:
|
||||||
return {
|
return {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
@@ -591,7 +707,7 @@ class ChuniBase:
|
|||||||
"userRecentRatingList": recent_rating_list["recentRating"],
|
"userRecentRatingList": recent_rating_list["recentRating"],
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_region_api_request(self, data: Dict) -> Dict:
|
async def handle_get_user_region_api_request(self, data: Dict) -> Dict:
|
||||||
# TODO: Region
|
# TODO: Region
|
||||||
return {
|
return {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
@@ -599,43 +715,99 @@ class ChuniBase:
|
|||||||
"userRegionList": [],
|
"userRegionList": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_team_api_request(self, data: Dict) -> Dict:
|
async def handle_get_user_team_api_request(self, data: Dict) -> Dict:
|
||||||
# TODO: use the database "chuni_profile_team" with a GUI
|
# Default values
|
||||||
|
team_id = 65535
|
||||||
team_name = self.game_cfg.team.team_name
|
team_name = self.game_cfg.team.team_name
|
||||||
if team_name == "":
|
team_rank = 0
|
||||||
|
|
||||||
|
# Get user profile
|
||||||
|
profile = await self.data.profile.get_profile_data(data["userId"], self.version)
|
||||||
|
if profile and profile["teamId"]:
|
||||||
|
# Get team by id
|
||||||
|
team = await self.data.profile.get_team_by_id(profile["teamId"])
|
||||||
|
|
||||||
|
if team:
|
||||||
|
team_id = team["id"]
|
||||||
|
team_name = team["teamName"]
|
||||||
|
team_rank = await self.data.profile.get_team_rank(team["id"])
|
||||||
|
|
||||||
|
# Don't return anything if no team name has been defined for defaults and there is no team set for the player
|
||||||
|
if not profile["teamId"] and team_name == "":
|
||||||
return {"userId": data["userId"], "teamId": 0}
|
return {"userId": data["userId"], "teamId": 0}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
"teamId": 1,
|
"teamId": team_id,
|
||||||
"teamRank": 1,
|
"teamRank": team_rank,
|
||||||
"teamName": team_name,
|
"teamName": team_name,
|
||||||
"userTeamPoint": {
|
"userTeamPoint": {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
"teamId": 1,
|
"teamId": team_id,
|
||||||
"orderId": 1,
|
"orderId": 1,
|
||||||
"teamPoint": 1,
|
"teamPoint": 1,
|
||||||
"aggrDate": data["playDate"],
|
"aggrDate": data["playDate"],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_team_course_setting_api_request(self, data: Dict) -> Dict:
|
async def handle_get_team_course_setting_api_request(self, data: Dict) -> Dict:
|
||||||
return {
|
return {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
"length": 0,
|
"length": 0,
|
||||||
"nextIndex": 0,
|
"nextIndex": -1,
|
||||||
"teamCourseSettingList": [],
|
"teamCourseSettingList": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_team_course_rule_api_request(self, data: Dict) -> Dict:
|
async def handle_get_team_course_setting_api_request_proto(self, data: Dict) -> Dict:
|
||||||
|
return {
|
||||||
|
"userId": data["userId"],
|
||||||
|
"length": 1,
|
||||||
|
"nextIndex": -1,
|
||||||
|
"teamCourseSettingList": [
|
||||||
|
{
|
||||||
|
"orderId": 1,
|
||||||
|
"courseId": 1,
|
||||||
|
"classId": 1,
|
||||||
|
"ruleId": 1,
|
||||||
|
"courseName": "Test",
|
||||||
|
"teamCourseMusicList": [
|
||||||
|
{"track": 184, "type": 1, "level": 3, "selectLevel": -1},
|
||||||
|
{"track": 184, "type": 1, "level": 3, "selectLevel": -1},
|
||||||
|
{"track": 184, "type": 1, "level": 3, "selectLevel": -1}
|
||||||
|
],
|
||||||
|
"teamCourseRankingInfoList": [],
|
||||||
|
"recodeDate": "2099-12-31 11:59:99.0",
|
||||||
|
"isPlayed": False
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def handle_get_team_course_rule_api_request(self, data: Dict) -> Dict:
|
||||||
return {
|
return {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
"length": 0,
|
"length": 0,
|
||||||
"nextIndex": 0,
|
"nextIndex": -1,
|
||||||
"teamCourseRuleList": [],
|
"teamCourseRuleList": []
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_upsert_user_all_api_request(self, data: Dict) -> Dict:
|
async def handle_get_team_course_rule_api_request_proto(self, data: Dict) -> Dict:
|
||||||
|
return {
|
||||||
|
"userId": data["userId"],
|
||||||
|
"length": 1,
|
||||||
|
"nextIndex": -1,
|
||||||
|
"teamCourseRuleList": [
|
||||||
|
{
|
||||||
|
"recoveryLife": 0,
|
||||||
|
"clearLife": 100,
|
||||||
|
"damageMiss": 1,
|
||||||
|
"damageAttack": 1,
|
||||||
|
"damageJustice": 1,
|
||||||
|
"damageJusticeC": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def handle_upsert_user_all_api_request(self, data: Dict) -> Dict:
|
||||||
upsert = data["upsertUserAll"]
|
upsert = data["upsertUserAll"]
|
||||||
user_id = data["userId"]
|
user_id = data["userId"]
|
||||||
|
|
||||||
@@ -647,113 +819,147 @@ class ChuniBase:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
self.data.profile.put_profile_data(
|
await self.data.profile.put_profile_data(
|
||||||
user_id, self.version, upsert["userData"][0]
|
user_id, self.version, upsert["userData"][0]
|
||||||
)
|
)
|
||||||
|
|
||||||
if "userDataEx" in upsert:
|
if "userDataEx" in upsert:
|
||||||
self.data.profile.put_profile_data_ex(
|
await self.data.profile.put_profile_data_ex(
|
||||||
user_id, self.version, upsert["userDataEx"][0]
|
user_id, self.version, upsert["userDataEx"][0]
|
||||||
)
|
)
|
||||||
|
|
||||||
if "userGameOption" in upsert:
|
if "userGameOption" in upsert:
|
||||||
self.data.profile.put_profile_option(user_id, upsert["userGameOption"][0])
|
await self.data.profile.put_profile_option(user_id, upsert["userGameOption"][0])
|
||||||
|
|
||||||
if "userGameOptionEx" in upsert:
|
if "userGameOptionEx" in upsert:
|
||||||
self.data.profile.put_profile_option_ex(
|
await self.data.profile.put_profile_option_ex(
|
||||||
user_id, upsert["userGameOptionEx"][0]
|
user_id, upsert["userGameOptionEx"][0]
|
||||||
)
|
)
|
||||||
if "userRecentRatingList" in upsert:
|
if "userRecentRatingList" in upsert:
|
||||||
self.data.profile.put_profile_recent_rating(
|
await self.data.profile.put_profile_recent_rating(
|
||||||
user_id, upsert["userRecentRatingList"]
|
user_id, upsert["userRecentRatingList"]
|
||||||
)
|
)
|
||||||
|
|
||||||
if "userCharacterList" in upsert:
|
if "userCharacterList" in upsert:
|
||||||
for character in upsert["userCharacterList"]:
|
for character in upsert["userCharacterList"]:
|
||||||
self.data.item.put_character(user_id, character)
|
await self.data.item.put_character(user_id, character)
|
||||||
|
|
||||||
if "userMapList" in upsert:
|
if "userMapList" in upsert:
|
||||||
for map in upsert["userMapList"]:
|
for map in upsert["userMapList"]:
|
||||||
self.data.item.put_map(user_id, map)
|
await self.data.item.put_map(user_id, map)
|
||||||
|
|
||||||
if "userCourseList" in upsert:
|
if "userCourseList" in upsert:
|
||||||
for course in upsert["userCourseList"]:
|
for course in upsert["userCourseList"]:
|
||||||
self.data.score.put_course(user_id, course)
|
await self.data.score.put_course(user_id, course)
|
||||||
|
|
||||||
if "userDuelList" in upsert:
|
if "userDuelList" in upsert:
|
||||||
for duel in upsert["userDuelList"]:
|
for duel in upsert["userDuelList"]:
|
||||||
self.data.item.put_duel(user_id, duel)
|
await self.data.item.put_duel(user_id, duel)
|
||||||
|
|
||||||
if "userItemList" in upsert:
|
if "userItemList" in upsert:
|
||||||
for item in upsert["userItemList"]:
|
for item in upsert["userItemList"]:
|
||||||
self.data.item.put_item(user_id, item)
|
await self.data.item.put_item(user_id, item)
|
||||||
|
|
||||||
if "userActivityList" in upsert:
|
if "userActivityList" in upsert:
|
||||||
for activity in upsert["userActivityList"]:
|
for activity in upsert["userActivityList"]:
|
||||||
self.data.profile.put_profile_activity(user_id, activity)
|
await self.data.profile.put_profile_activity(user_id, activity)
|
||||||
|
|
||||||
if "userChargeList" in upsert:
|
if "userChargeList" in upsert:
|
||||||
for charge in upsert["userChargeList"]:
|
for charge in upsert["userChargeList"]:
|
||||||
self.data.profile.put_profile_charge(user_id, charge)
|
await self.data.profile.put_profile_charge(user_id, charge)
|
||||||
|
|
||||||
if "userMusicDetailList" in upsert:
|
if "userMusicDetailList" in upsert:
|
||||||
for song in upsert["userMusicDetailList"]:
|
for song in upsert["userMusicDetailList"]:
|
||||||
self.data.score.put_score(user_id, song)
|
await self.data.score.put_score(user_id, song)
|
||||||
|
|
||||||
if "userPlaylogList" in upsert:
|
if "userPlaylogList" in upsert:
|
||||||
for playlog in upsert["userPlaylogList"]:
|
for playlog in upsert["userPlaylogList"]:
|
||||||
# convert the player names to utf-8
|
# convert the player names to utf-8
|
||||||
playlog["playedUserName1"] = self.read_wtf8(playlog["playedUserName1"])
|
if playlog["playedUserName1"] is not None:
|
||||||
playlog["playedUserName2"] = self.read_wtf8(playlog["playedUserName2"])
|
playlog["playedUserName1"] = self.read_wtf8(playlog["playedUserName1"])
|
||||||
playlog["playedUserName3"] = self.read_wtf8(playlog["playedUserName3"])
|
if playlog["playedUserName2"] is not None:
|
||||||
self.data.score.put_playlog(user_id, playlog)
|
playlog["playedUserName2"] = self.read_wtf8(playlog["playedUserName2"])
|
||||||
|
if playlog["playedUserName3"] is not None:
|
||||||
|
playlog["playedUserName3"] = self.read_wtf8(playlog["playedUserName3"])
|
||||||
|
await self.data.score.put_playlog(user_id, playlog, self.version)
|
||||||
|
|
||||||
if "userTeamPoint" in upsert:
|
if "userTeamPoint" in upsert:
|
||||||
# TODO: team stuff
|
team_points = upsert["userTeamPoint"]
|
||||||
pass
|
try:
|
||||||
|
for tp in team_points:
|
||||||
|
if tp["teamId"] != '65535':
|
||||||
|
# Fetch the current team data
|
||||||
|
current_team = await self.data.profile.get_team_by_id(tp["teamId"])
|
||||||
|
|
||||||
|
# Calculate the new teamPoint
|
||||||
|
new_team_point = int(tp["teamPoint"]) + current_team["teamPoint"]
|
||||||
|
|
||||||
|
# Prepare the data to update
|
||||||
|
team_data = {
|
||||||
|
"teamPoint": new_team_point
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update the team data
|
||||||
|
await self.data.profile.update_team(tp["teamId"], team_data)
|
||||||
|
except:
|
||||||
|
pass # Probably a better way to catch if the team is not set yet (new profiles), but let's just pass
|
||||||
if "userMapAreaList" in upsert:
|
if "userMapAreaList" in upsert:
|
||||||
for map_area in upsert["userMapAreaList"]:
|
for map_area in upsert["userMapAreaList"]:
|
||||||
self.data.item.put_map_area(user_id, map_area)
|
await self.data.item.put_map_area(user_id, map_area)
|
||||||
|
|
||||||
if "userOverPowerList" in upsert:
|
if "userOverPowerList" in upsert:
|
||||||
for overpower in upsert["userOverPowerList"]:
|
for overpower in upsert["userOverPowerList"]:
|
||||||
self.data.profile.put_profile_overpower(user_id, overpower)
|
await self.data.profile.put_profile_overpower(user_id, overpower)
|
||||||
|
|
||||||
if "userEmoneyList" in upsert:
|
if "userEmoneyList" in upsert:
|
||||||
for emoney in upsert["userEmoneyList"]:
|
for emoney in upsert["userEmoneyList"]:
|
||||||
self.data.profile.put_profile_emoney(user_id, emoney)
|
await self.data.profile.put_profile_emoney(user_id, emoney)
|
||||||
|
|
||||||
if "userLoginBonusList" in upsert:
|
if "userLoginBonusList" in upsert:
|
||||||
for login in upsert["userLoginBonusList"]:
|
for login in upsert["userLoginBonusList"]:
|
||||||
self.data.item.put_login_bonus(
|
await self.data.item.put_login_bonus(
|
||||||
user_id, self.version, login["presetId"], isWatched=True
|
user_id, self.version, login["presetId"], isWatched=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if "userRecentPlayerList" in upsert: # TODO: Seen in Air, maybe implement sometime
|
||||||
|
for rp in upsert["userRecentPlayerList"]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
for rating_type in {"userRatingBaseList", "userRatingBaseHotList", "userRatingBaseNextList"}:
|
||||||
|
if rating_type not in upsert:
|
||||||
|
continue
|
||||||
|
|
||||||
|
await self.data.profile.put_profile_rating(
|
||||||
|
user_id,
|
||||||
|
self.version,
|
||||||
|
rating_type,
|
||||||
|
upsert[rating_type],
|
||||||
|
)
|
||||||
|
|
||||||
return {"returnCode": "1"}
|
return {"returnCode": "1"}
|
||||||
|
|
||||||
def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict:
|
async def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict:
|
||||||
# add tickets after they got bought, this makes sure the tickets are
|
# add tickets after they got bought, this makes sure the tickets are
|
||||||
# still valid after an unsuccessful logout
|
# still valid after an unsuccessful logout
|
||||||
self.data.profile.put_profile_charge(data["userId"], data["userCharge"])
|
await self.data.profile.put_profile_charge(data["userId"], data["userCharge"])
|
||||||
return {"returnCode": "1"}
|
return {"returnCode": "1"}
|
||||||
|
|
||||||
def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict:
|
async def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict:
|
||||||
return {"returnCode": "1"}
|
return {"returnCode": "1"}
|
||||||
|
|
||||||
def handle_upsert_client_develop_api_request(self, data: Dict) -> Dict:
|
async def handle_upsert_client_develop_api_request(self, data: Dict) -> Dict:
|
||||||
return {"returnCode": "1"}
|
return {"returnCode": "1"}
|
||||||
|
|
||||||
def handle_upsert_client_error_api_request(self, data: Dict) -> Dict:
|
async def handle_upsert_client_error_api_request(self, data: Dict) -> Dict:
|
||||||
return {"returnCode": "1"}
|
return {"returnCode": "1"}
|
||||||
|
|
||||||
def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict:
|
async def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict:
|
||||||
return {"returnCode": "1"}
|
return {"returnCode": "1"}
|
||||||
|
|
||||||
def handle_upsert_client_testmode_api_request(self, data: Dict) -> Dict:
|
async def handle_upsert_client_testmode_api_request(self, data: Dict) -> Dict:
|
||||||
return {"returnCode": "1"}
|
return {"returnCode": "1"}
|
||||||
|
|
||||||
def handle_get_user_net_battle_data_api_request(self, data: Dict) -> Dict:
|
async def handle_get_user_net_battle_data_api_request(self, data: Dict) -> Dict:
|
||||||
return {
|
return {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
"userNetBattleData": {"recentNBSelectMusicList": []},
|
"userNetBattleData": {"recentNBSelectMusicList": []},
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ class ChuniServerConfig:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def news_msg(self) -> str:
|
||||||
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "chuni", "server", "news_msg", default=""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ChuniTeamConfig:
|
class ChuniTeamConfig:
|
||||||
def __init__(self, parent_config: "ChuniConfig") -> None:
|
def __init__(self, parent_config: "ChuniConfig") -> None:
|
||||||
@@ -30,6 +36,11 @@ class ChuniTeamConfig:
|
|||||||
return CoreConfig.get_config_field(
|
return CoreConfig.get_config_field(
|
||||||
self.__config, "chuni", "team", "name", default=""
|
self.__config, "chuni", "team", "name", default=""
|
||||||
)
|
)
|
||||||
|
@property
|
||||||
|
def rank_scale(self) -> str:
|
||||||
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "chuni", "team", "rank_scale", default="False"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ChuniModsConfig:
|
class ChuniModsConfig:
|
||||||
@@ -78,6 +89,27 @@ class ChuniCryptoConfig:
|
|||||||
self.__config, "chuni", "crypto", "encrypted_only", default=False
|
self.__config, "chuni", "crypto", "encrypted_only", default=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class ChuniMatchingConfig:
|
||||||
|
def __init__(self, parent_config: "ChuniConfig") -> None:
|
||||||
|
self.__config = parent_config
|
||||||
|
|
||||||
|
@property
|
||||||
|
def enable(self) -> bool:
|
||||||
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "chuni", "matching", "enable", default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def match_time_limit(self) -> int:
|
||||||
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "chuni", "matching", "match_time_limit", default=60
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def match_error_limit(self) -> int:
|
||||||
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "chuni", "matching", "match_error_limit", default=9999
|
||||||
|
)
|
||||||
|
|
||||||
class ChuniConfig(dict):
|
class ChuniConfig(dict):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
@@ -86,3 +118,4 @@ class ChuniConfig(dict):
|
|||||||
self.mods = ChuniModsConfig(self)
|
self.mods = ChuniModsConfig(self)
|
||||||
self.version = ChuniVersionConfig(self)
|
self.version = ChuniVersionConfig(self)
|
||||||
self.crypto = ChuniCryptoConfig(self)
|
self.crypto = ChuniCryptoConfig(self)
|
||||||
|
self.matching = ChuniMatchingConfig(self)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
class ChuniConstants:
|
class ChuniConstants:
|
||||||
GAME_CODE = "SDBT"
|
GAME_CODE = "SDBT"
|
||||||
GAME_CODE_NEW = "SDHD"
|
GAME_CODE_NEW = "SDHD"
|
||||||
|
GAME_CODE_INT = "SDGS"
|
||||||
|
|
||||||
CONFIG_NAME = "chuni.yaml"
|
CONFIG_NAME = "chuni.yaml"
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ class ChuniConstants:
|
|||||||
VER_CHUNITHM_NEW = 11
|
VER_CHUNITHM_NEW = 11
|
||||||
VER_CHUNITHM_NEW_PLUS = 12
|
VER_CHUNITHM_NEW_PLUS = 12
|
||||||
VER_CHUNITHM_SUN = 13
|
VER_CHUNITHM_SUN = 13
|
||||||
|
VER_CHUNITHM_SUN_PLUS = 14
|
||||||
VERSION_NAMES = [
|
VERSION_NAMES = [
|
||||||
"CHUNITHM",
|
"CHUNITHM",
|
||||||
"CHUNITHM PLUS",
|
"CHUNITHM PLUS",
|
||||||
@@ -33,7 +34,8 @@ class ChuniConstants:
|
|||||||
"CHUNITHM PARADISE",
|
"CHUNITHM PARADISE",
|
||||||
"CHUNITHM NEW!!",
|
"CHUNITHM NEW!!",
|
||||||
"CHUNITHM NEW PLUS!!",
|
"CHUNITHM NEW PLUS!!",
|
||||||
"CHUNITHM SUN"
|
"CHUNITHM SUN",
|
||||||
|
"CHUNITHM SUN PLUS"
|
||||||
]
|
]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class ChuniCrystal(ChuniBase):
|
|||||||
super().__init__(core_cfg, game_cfg)
|
super().__init__(core_cfg, game_cfg)
|
||||||
self.version = ChuniConstants.VER_CHUNITHM_CRYSTAL
|
self.version = ChuniConstants.VER_CHUNITHM_CRYSTAL
|
||||||
|
|
||||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
async def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||||
ret = super().handle_get_game_setting_api_request(data)
|
ret = await super().handle_get_game_setting_api_request(data)
|
||||||
ret["gameSetting"]["dataVersion"] = "1.40.00"
|
ret["gameSetting"]["dataVersion"] = "1.40.00"
|
||||||
return ret
|
return ret
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class ChuniCrystalPlus(ChuniBase):
|
|||||||
super().__init__(core_cfg, game_cfg)
|
super().__init__(core_cfg, game_cfg)
|
||||||
self.version = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS
|
self.version = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS
|
||||||
|
|
||||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
async def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||||
ret = super().handle_get_game_setting_api_request(data)
|
ret = await super().handle_get_game_setting_api_request(data)
|
||||||
ret["gameSetting"]["dataVersion"] = "1.45.00"
|
ret["gameSetting"]["dataVersion"] = "1.45.00"
|
||||||
return ret
|
return ret
|
||||||
|
|||||||
83
titles/chuni/frontend.py
Normal file
83
titles/chuni/frontend.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
from typing import List
|
||||||
|
from starlette.routing import Route
|
||||||
|
from starlette.requests import Request
|
||||||
|
from starlette.responses import Response, RedirectResponse
|
||||||
|
from os import path
|
||||||
|
import yaml
|
||||||
|
import jinja2
|
||||||
|
|
||||||
|
from core.frontend import FE_Base, UserSession
|
||||||
|
from core.config import CoreConfig
|
||||||
|
from .database import ChuniData
|
||||||
|
from .config import ChuniConfig
|
||||||
|
from .const import ChuniConstants
|
||||||
|
|
||||||
|
|
||||||
|
class ChuniFrontend(FE_Base):
|
||||||
|
def __init__(
|
||||||
|
self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str
|
||||||
|
) -> None:
|
||||||
|
super().__init__(cfg, environment)
|
||||||
|
self.data = ChuniData(cfg)
|
||||||
|
self.game_cfg = ChuniConfig()
|
||||||
|
if path.exists(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"):
|
||||||
|
self.game_cfg.update(
|
||||||
|
yaml.safe_load(open(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"))
|
||||||
|
)
|
||||||
|
self.nav_name = "Chunithm"
|
||||||
|
|
||||||
|
def get_routes(self) -> List[Route]:
|
||||||
|
return [
|
||||||
|
Route("/", self.render_GET, methods=['GET']),
|
||||||
|
Route("/update.name", self.update_name, methods=['POST']),
|
||||||
|
]
|
||||||
|
|
||||||
|
async def render_GET(self, request: Request) -> bytes:
|
||||||
|
template = self.environment.get_template(
|
||||||
|
"titles/chuni/templates/chuni_index.jinja"
|
||||||
|
)
|
||||||
|
usr_sesh = self.validate_session(request)
|
||||||
|
if not usr_sesh:
|
||||||
|
usr_sesh = UserSession()
|
||||||
|
|
||||||
|
return Response(template.render(
|
||||||
|
title=f"{self.core_config.server.name} | {self.nav_name}",
|
||||||
|
game_list=self.environment.globals["game_list"],
|
||||||
|
sesh=vars(usr_sesh)
|
||||||
|
), media_type="text/html; charset=utf-8")
|
||||||
|
|
||||||
|
async def update_name(self, request: Request) -> bytes:
|
||||||
|
usr_sesh = self.validate_session(request)
|
||||||
|
if not usr_sesh:
|
||||||
|
return RedirectResponse("/gate/", 303)
|
||||||
|
|
||||||
|
new_name: str = request.query_params.get('new_name', '')
|
||||||
|
new_name_full = ""
|
||||||
|
|
||||||
|
if not new_name:
|
||||||
|
return RedirectResponse("/gate/?e=4", 303)
|
||||||
|
|
||||||
|
if len(new_name) > 8:
|
||||||
|
return RedirectResponse("/gate/?e=8", 303)
|
||||||
|
|
||||||
|
for x in new_name: # FIXME: This will let some invalid characters through atm
|
||||||
|
o = ord(x)
|
||||||
|
try:
|
||||||
|
if o == 0x20:
|
||||||
|
new_name_full += chr(0x3000)
|
||||||
|
elif o < 0x7F and o > 0x20:
|
||||||
|
new_name_full += chr(o + 0xFEE0)
|
||||||
|
elif o <= 0x7F:
|
||||||
|
self.logger.warn(f"Invalid ascii character {o:02X}")
|
||||||
|
return RedirectResponse("/gate/?e=4", 303)
|
||||||
|
else:
|
||||||
|
new_name_full += x
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Something went wrong parsing character {o:04X} - {e}")
|
||||||
|
return RedirectResponse("/gate/?e=4", 303)
|
||||||
|
|
||||||
|
if not await self.data.profile.update_name(usr_sesh, new_name_full):
|
||||||
|
return RedirectResponse("/gate/?e=999", 303)
|
||||||
|
|
||||||
|
return RedirectResponse("/gate/?s=1", 303)
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
from twisted.web.http import Request
|
from starlette.requests import Request
|
||||||
|
from starlette.routing import Route
|
||||||
|
from starlette.responses import Response
|
||||||
import logging, coloredlogs
|
import logging, coloredlogs
|
||||||
from logging.handlers import TimedRotatingFileHandler
|
from logging.handlers import TimedRotatingFileHandler
|
||||||
import zlib
|
import zlib
|
||||||
@@ -11,30 +13,31 @@ from Crypto.Util.Padding import pad
|
|||||||
from Crypto.Protocol.KDF import PBKDF2
|
from Crypto.Protocol.KDF import PBKDF2
|
||||||
from Crypto.Hash import SHA1
|
from Crypto.Hash import SHA1
|
||||||
from os import path
|
from os import path
|
||||||
from typing import Tuple, Dict
|
from typing import Tuple, Dict, List
|
||||||
|
|
||||||
from core import CoreConfig, Utils
|
from core import CoreConfig, Utils
|
||||||
from titles.chuni.config import ChuniConfig
|
from core.title import BaseServlet
|
||||||
from titles.chuni.const import ChuniConstants
|
from .config import ChuniConfig
|
||||||
from titles.chuni.base import ChuniBase
|
from .const import ChuniConstants
|
||||||
from titles.chuni.plus import ChuniPlus
|
from .base import ChuniBase
|
||||||
from titles.chuni.air import ChuniAir
|
from .plus import ChuniPlus
|
||||||
from titles.chuni.airplus import ChuniAirPlus
|
from .air import ChuniAir
|
||||||
from titles.chuni.star import ChuniStar
|
from .airplus import ChuniAirPlus
|
||||||
from titles.chuni.starplus import ChuniStarPlus
|
from .star import ChuniStar
|
||||||
from titles.chuni.amazon import ChuniAmazon
|
from .starplus import ChuniStarPlus
|
||||||
from titles.chuni.amazonplus import ChuniAmazonPlus
|
from .amazon import ChuniAmazon
|
||||||
from titles.chuni.crystal import ChuniCrystal
|
from .amazonplus import ChuniAmazonPlus
|
||||||
from titles.chuni.crystalplus import ChuniCrystalPlus
|
from .crystal import ChuniCrystal
|
||||||
from titles.chuni.paradise import ChuniParadise
|
from .crystalplus import ChuniCrystalPlus
|
||||||
from titles.chuni.new import ChuniNew
|
from .paradise import ChuniParadise
|
||||||
from titles.chuni.newplus import ChuniNewPlus
|
from .new import ChuniNew
|
||||||
from titles.chuni.sun import ChuniSun
|
from .newplus import ChuniNewPlus
|
||||||
|
from .sun import ChuniSun
|
||||||
|
from .sunplus import ChuniSunPlus
|
||||||
|
|
||||||
|
class ChuniServlet(BaseServlet):
|
||||||
class ChuniServlet:
|
|
||||||
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
|
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
|
||||||
self.core_cfg = core_cfg
|
super().__init__(core_cfg, cfg_dir)
|
||||||
self.game_cfg = ChuniConfig()
|
self.game_cfg = ChuniConfig()
|
||||||
self.hash_table: Dict[Dict[str, str]] = {}
|
self.hash_table: Dict[Dict[str, str]] = {}
|
||||||
if path.exists(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"):
|
if path.exists(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"):
|
||||||
@@ -57,6 +60,7 @@ class ChuniServlet:
|
|||||||
ChuniNew,
|
ChuniNew,
|
||||||
ChuniNewPlus,
|
ChuniNewPlus,
|
||||||
ChuniSun,
|
ChuniSun,
|
||||||
|
ChuniSunPlus,
|
||||||
]
|
]
|
||||||
|
|
||||||
self.logger = logging.getLogger("chuni")
|
self.logger = logging.getLogger("chuni")
|
||||||
@@ -98,8 +102,14 @@ class ChuniServlet:
|
|||||||
]
|
]
|
||||||
for method in method_list:
|
for method in method_list:
|
||||||
method_fixed = inflection.camelize(method)[6:-7]
|
method_fixed = inflection.camelize(method)[6:-7]
|
||||||
# number of iterations was changed to 70 in SUN
|
# number of iterations was changed to 70 in SUN and then to 36
|
||||||
iter_count = 70 if version >= ChuniConstants.VER_CHUNITHM_SUN else 44
|
if version == ChuniConstants.VER_CHUNITHM_SUN_PLUS:
|
||||||
|
iter_count = 36
|
||||||
|
elif version == ChuniConstants.VER_CHUNITHM_SUN:
|
||||||
|
iter_count = 70
|
||||||
|
else:
|
||||||
|
iter_count = 44
|
||||||
|
|
||||||
hash = PBKDF2(
|
hash = PBKDF2(
|
||||||
method_fixed,
|
method_fixed,
|
||||||
bytes.fromhex(keys[2]),
|
bytes.fromhex(keys[2]),
|
||||||
@@ -116,9 +126,9 @@ class ChuniServlet:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_allnet_info(
|
def is_game_enabled(
|
||||||
cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str
|
cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str
|
||||||
) -> Tuple[bool, str, str]:
|
) -> bool:
|
||||||
game_cfg = ChuniConfig()
|
game_cfg = ChuniConfig()
|
||||||
if path.exists(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"):
|
if path.exists(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"):
|
||||||
game_cfg.update(
|
game_cfg.update(
|
||||||
@@ -126,76 +136,98 @@ class ChuniServlet:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not game_cfg.server.enable:
|
if not game_cfg.server.enable:
|
||||||
return (False, "", "")
|
return False
|
||||||
|
|
||||||
if core_cfg.server.is_develop:
|
return True
|
||||||
return (
|
|
||||||
True,
|
|
||||||
f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/",
|
|
||||||
"",
|
|
||||||
)
|
|
||||||
|
|
||||||
return (True, f"http://{core_cfg.title.hostname}/{game_code}/$v/", "")
|
def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]:
|
||||||
|
if not self.core_cfg.server.is_using_proxy and Utils.get_title_port(self.core_cfg) != 80:
|
||||||
|
return (f"http://{self.core_cfg.server.hostname}:{Utils.get_title_port(self.core_cfg)}/{game_code}/{game_ver}/", self.core_cfg.server.hostname)
|
||||||
|
|
||||||
def render_POST(self, request: Request, version: int, url_path: str) -> bytes:
|
return (f"http://{self.core_cfg.server.hostname}/{game_code}/{game_ver}/", self.core_cfg.server.hostname)
|
||||||
if url_path.lower() == "ping":
|
|
||||||
return zlib.compress(b'{"returnCode": "1"}')
|
def get_routes(self) -> List[Route]:
|
||||||
|
return [
|
||||||
|
Route("/{game:str}/{version:int}/ChuniServlet/{endpoint:str}", self.render_POST, methods=['POST']),
|
||||||
|
Route("/{game:str}/{version:int}/ChuniServlet/MatchingServer/{endpoint:str}", self.render_POST, methods=['POST']),
|
||||||
|
]
|
||||||
|
|
||||||
|
async def render_POST(self, request: Request) -> bytes:
|
||||||
|
endpoint: str = request.path_params.get('endpoint')
|
||||||
|
version: int = request.path_params.get('version')
|
||||||
|
game_code: str = request.path_params.get('game')
|
||||||
|
|
||||||
|
if endpoint.lower() == "ping":
|
||||||
|
return Response(zlib.compress(b'{"returnCode": "1"}'))
|
||||||
|
|
||||||
|
req_raw = await request.body()
|
||||||
|
|
||||||
req_raw = request.content.getvalue()
|
|
||||||
url_split = url_path.split("/")
|
|
||||||
encrtped = False
|
encrtped = False
|
||||||
internal_ver = 0
|
internal_ver = 0
|
||||||
endpoint = url_split[len(url_split) - 1]
|
|
||||||
client_ip = Utils.get_ip_addr(request)
|
client_ip = Utils.get_ip_addr(request)
|
||||||
|
|
||||||
if version < 105: # 1.0
|
if game_code == "SDHD" or game_code == "SDBT": # JP
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM
|
if version < 105: # 1.0
|
||||||
elif version >= 105 and version < 110: # PLUS
|
internal_ver = ChuniConstants.VER_CHUNITHM
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM_PLUS
|
elif version >= 105 and version < 110: # PLUS
|
||||||
elif version >= 110 and version < 115: # AIR
|
internal_ver = ChuniConstants.VER_CHUNITHM_PLUS
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM_AIR
|
elif version >= 110 and version < 115: # AIR
|
||||||
elif version >= 115 and version < 120: # AIR PLUS
|
internal_ver = ChuniConstants.VER_CHUNITHM_AIR
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM_AIR_PLUS
|
elif version >= 115 and version < 120: # AIR PLUS
|
||||||
elif version >= 120 and version < 125: # STAR
|
internal_ver = ChuniConstants.VER_CHUNITHM_AIR_PLUS
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM_STAR
|
elif version >= 120 and version < 125: # STAR
|
||||||
elif version >= 125 and version < 130: # STAR PLUS
|
internal_ver = ChuniConstants.VER_CHUNITHM_STAR
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM_STAR_PLUS
|
elif version >= 125 and version < 130: # STAR PLUS
|
||||||
elif version >= 130 and version < 135: # AMAZON
|
internal_ver = ChuniConstants.VER_CHUNITHM_STAR_PLUS
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON
|
elif version >= 130 and version < 135: # AMAZON
|
||||||
elif version >= 135 and version < 140: # AMAZON PLUS
|
internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON_PLUS
|
elif version >= 135 and version < 140: # AMAZON PLUS
|
||||||
elif version >= 140 and version < 145: # CRYSTAL
|
internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON_PLUS
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL
|
elif version >= 140 and version < 145: # CRYSTAL
|
||||||
elif version >= 145 and version < 150: # CRYSTAL PLUS
|
internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS
|
elif version >= 145 and version < 150: # CRYSTAL PLUS
|
||||||
elif version >= 150 and version < 200: # PARADISE
|
internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE
|
elif version >= 150 and version < 200: # PARADISE
|
||||||
elif version >= 200 and version < 205: # NEW!!
|
internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM_NEW
|
elif version >= 200 and version < 205: # NEW!!
|
||||||
elif version >= 205 and version < 210: # NEW PLUS!!
|
internal_ver = ChuniConstants.VER_CHUNITHM_NEW
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS
|
elif version >= 205 and version < 210: # NEW PLUS!!
|
||||||
elif version >= 210: # SUN
|
internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM_SUN
|
elif version >= 210 and version < 215: # SUN
|
||||||
|
internal_ver = ChuniConstants.VER_CHUNITHM_SUN
|
||||||
|
elif version >= 215: # SUN
|
||||||
|
internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS
|
||||||
|
elif game_code == "SDGS": # Int
|
||||||
|
if version < 110: # SUPERSTAR
|
||||||
|
internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE # FIXME: Not sure what was intended to go here? was just "PARADISE"
|
||||||
|
elif version >= 110 and version < 115: # NEW
|
||||||
|
internal_ver = ChuniConstants.VER_CHUNITHM_NEW
|
||||||
|
elif version >= 115 and version < 120: # NEW PLUS!!
|
||||||
|
internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS
|
||||||
|
elif version >= 120 and version < 125: # SUN
|
||||||
|
internal_ver = ChuniConstants.VER_CHUNITHM_SUN
|
||||||
|
elif version >= 125: # SUN PLUS
|
||||||
|
internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS
|
||||||
|
|
||||||
if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32:
|
if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32:
|
||||||
# If we get a 32 character long hex string, it's a hash and we're
|
# If we get a 32 character long hex string, it's a hash and we're
|
||||||
# doing encrypted. The likelyhood of false positives is low but
|
# doing encrypted. The likelyhood of false positives is low but
|
||||||
# technically not 0
|
# technically not 0
|
||||||
if internal_ver < ChuniConstants.VER_CHUNITHM_NEW:
|
if internal_ver < ChuniConstants.VER_CHUNITHM_NEW:
|
||||||
endpoint = request.getHeader("User-Agent").split("#")[0]
|
endpoint = request.headers.get("User-Agent").split("#")[0]
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if internal_ver not in self.hash_table:
|
if internal_ver not in self.hash_table:
|
||||||
self.logger.error(
|
self.logger.error(
|
||||||
f"v{version} does not support encryption or no keys entered"
|
f"v{version} does not support encryption or no keys entered"
|
||||||
)
|
)
|
||||||
return zlib.compress(b'{"stat": "0"}')
|
return Response(zlib.compress(b'{"stat": "0"}'))
|
||||||
|
|
||||||
elif endpoint.lower() not in self.hash_table[internal_ver]:
|
elif endpoint.lower() not in self.hash_table[internal_ver]:
|
||||||
self.logger.error(
|
self.logger.error(
|
||||||
f"No hash found for v{version} endpoint {endpoint}"
|
f"No hash found for v{version} endpoint {endpoint}"
|
||||||
)
|
)
|
||||||
return zlib.compress(b'{"stat": "0"}')
|
return Response(zlib.compress(b'{"stat": "0"}'))
|
||||||
|
|
||||||
endpoint = self.hash_table[internal_ver][endpoint.lower()]
|
endpoint = self.hash_table[internal_ver][endpoint.lower()]
|
||||||
|
|
||||||
@@ -212,7 +244,7 @@ class ChuniServlet:
|
|||||||
self.logger.error(
|
self.logger.error(
|
||||||
f"Failed to decrypt v{version} request to {endpoint} -> {e}"
|
f"Failed to decrypt v{version} request to {endpoint} -> {e}"
|
||||||
)
|
)
|
||||||
return zlib.compress(b'{"stat": "0"}')
|
return Response(zlib.compress(b'{"stat": "0"}'))
|
||||||
|
|
||||||
encrtped = True
|
encrtped = True
|
||||||
|
|
||||||
@@ -224,7 +256,7 @@ class ChuniServlet:
|
|||||||
self.logger.error(
|
self.logger.error(
|
||||||
f"Unencrypted v{version} {endpoint} request, but config is set to encrypted only: {req_raw}"
|
f"Unencrypted v{version} {endpoint} request, but config is set to encrypted only: {req_raw}"
|
||||||
)
|
)
|
||||||
return zlib.compress(b'{"stat": "0"}')
|
return Response(zlib.compress(b'{"stat": "0"}'))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
unzip = zlib.decompress(req_raw)
|
unzip = zlib.decompress(req_raw)
|
||||||
@@ -233,13 +265,14 @@ class ChuniServlet:
|
|||||||
self.logger.error(
|
self.logger.error(
|
||||||
f"Failed to decompress v{version} {endpoint} request -> {e}"
|
f"Failed to decompress v{version} {endpoint} request -> {e}"
|
||||||
)
|
)
|
||||||
return b""
|
return Response(zlib.compress(b'{"stat": "0"}'))
|
||||||
|
|
||||||
req_data = json.loads(unzip)
|
req_data = json.loads(unzip)
|
||||||
|
|
||||||
self.logger.info(f"v{version} {endpoint} request from {client_ip}")
|
self.logger.info(f"v{version} {endpoint} request from {client_ip}")
|
||||||
self.logger.debug(req_data)
|
self.logger.debug(req_data)
|
||||||
|
|
||||||
|
endpoint = endpoint.replace("C3Exp", "") if game_code == "SDGS" else endpoint
|
||||||
func_to_find = "handle_" + inflection.underscore(endpoint) + "_request"
|
func_to_find = "handle_" + inflection.underscore(endpoint) + "_request"
|
||||||
handler_cls = self.versions[internal_ver](self.core_cfg, self.game_cfg)
|
handler_cls = self.versions[internal_ver](self.core_cfg, self.game_cfg)
|
||||||
|
|
||||||
@@ -250,11 +283,11 @@ class ChuniServlet:
|
|||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
handler = getattr(handler_cls, func_to_find)
|
handler = getattr(handler_cls, func_to_find)
|
||||||
resp = handler(req_data)
|
resp = await handler(req_data)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Error handling v{version} method {endpoint} - {e}")
|
self.logger.error(f"Error handling v{version} method {endpoint} - {e}")
|
||||||
return zlib.compress(b'{"stat": "0"}')
|
return Response(zlib.compress(b'{"stat": "0"}'))
|
||||||
|
|
||||||
if resp == None:
|
if resp == None:
|
||||||
resp = {"returnCode": 1}
|
resp = {"returnCode": 1}
|
||||||
@@ -264,7 +297,7 @@ class ChuniServlet:
|
|||||||
zipped = zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8"))
|
zipped = zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8"))
|
||||||
|
|
||||||
if not encrtped:
|
if not encrtped:
|
||||||
return zipped
|
return Response(zipped)
|
||||||
|
|
||||||
padded = pad(zipped, 16)
|
padded = pad(zipped, 16)
|
||||||
|
|
||||||
@@ -274,4 +307,4 @@ class ChuniServlet:
|
|||||||
bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]),
|
bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]),
|
||||||
)
|
)
|
||||||
|
|
||||||
return crypt.encrypt(padded)
|
return Response(crypt.encrypt(padded))
|
||||||
@@ -3,13 +3,14 @@ from datetime import datetime, timedelta
|
|||||||
from random import randint
|
from random import randint
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
|
import pytz
|
||||||
from core.config import CoreConfig
|
from core.config import CoreConfig
|
||||||
|
from core.utils import Utils
|
||||||
from titles.chuni.const import ChuniConstants
|
from titles.chuni.const import ChuniConstants
|
||||||
from titles.chuni.database import ChuniData
|
from titles.chuni.database import ChuniData
|
||||||
from titles.chuni.base import ChuniBase
|
from titles.chuni.base import ChuniBase
|
||||||
from titles.chuni.config import ChuniConfig
|
from titles.chuni.config import ChuniConfig
|
||||||
|
|
||||||
|
|
||||||
class ChuniNew(ChuniBase):
|
class ChuniNew(ChuniBase):
|
||||||
ITEM_TYPE = {"character": 20, "story": 21, "card": 22}
|
ITEM_TYPE = {"character": 20, "story": 21, "card": 22}
|
||||||
|
|
||||||
@@ -22,7 +23,17 @@ class ChuniNew(ChuniBase):
|
|||||||
self.game = ChuniConstants.GAME_CODE
|
self.game = ChuniConstants.GAME_CODE
|
||||||
self.version = ChuniConstants.VER_CHUNITHM_NEW
|
self.version = ChuniConstants.VER_CHUNITHM_NEW
|
||||||
|
|
||||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
def _interal_ver_to_intver(self) -> str:
|
||||||
|
if self.version == ChuniConstants.VER_CHUNITHM_NEW:
|
||||||
|
return "200"
|
||||||
|
if self.version == ChuniConstants.VER_CHUNITHM_NEW_PLUS:
|
||||||
|
return "205"
|
||||||
|
if self.version == ChuniConstants.VER_CHUNITHM_SUN:
|
||||||
|
return "210"
|
||||||
|
if self.version == ChuniConstants.VER_CHUNITHM_SUN_PLUS:
|
||||||
|
return "215"
|
||||||
|
|
||||||
|
async def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||||
# use UTC time and convert it to JST time by adding +9
|
# use UTC time and convert it to JST time by adding +9
|
||||||
# matching therefore starts one hour before and lasts for 8 hours
|
# matching therefore starts one hour before and lasts for 8 hours
|
||||||
match_start = datetime.strftime(
|
match_start = datetime.strftime(
|
||||||
@@ -31,12 +42,30 @@ class ChuniNew(ChuniBase):
|
|||||||
match_end = datetime.strftime(
|
match_end = datetime.strftime(
|
||||||
datetime.utcnow() + timedelta(hours=16), self.date_time_format
|
datetime.utcnow() + timedelta(hours=16), self.date_time_format
|
||||||
)
|
)
|
||||||
reboot_start = datetime.strftime(
|
# if reboot start/end time is not defined use the default behavior of being a few hours ago
|
||||||
datetime.utcnow() + timedelta(hours=6), self.date_time_format
|
if self.core_cfg.title.reboot_start_time == "" or self.core_cfg.title.reboot_end_time == "":
|
||||||
)
|
reboot_start = datetime.strftime(
|
||||||
reboot_end = datetime.strftime(
|
datetime.utcnow() + timedelta(hours=6), self.date_time_format
|
||||||
datetime.utcnow() + timedelta(hours=7), self.date_time_format
|
)
|
||||||
)
|
reboot_end = datetime.strftime(
|
||||||
|
datetime.utcnow() + timedelta(hours=7), self.date_time_format
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# get current datetime in JST
|
||||||
|
current_jst = datetime.now(pytz.timezone('Asia/Tokyo')).date()
|
||||||
|
|
||||||
|
# parse config start/end times into datetime
|
||||||
|
reboot_start_time = datetime.strptime(self.core_cfg.title.reboot_start_time, "%H:%M")
|
||||||
|
reboot_end_time = datetime.strptime(self.core_cfg.title.reboot_end_time, "%H:%M")
|
||||||
|
|
||||||
|
# offset datetimes with current date/time
|
||||||
|
reboot_start_time = reboot_start_time.replace(year=current_jst.year, month=current_jst.month, day=current_jst.day, tzinfo=pytz.timezone('Asia/Tokyo'))
|
||||||
|
reboot_end_time = reboot_end_time.replace(year=current_jst.year, month=current_jst.month, day=current_jst.day, tzinfo=pytz.timezone('Asia/Tokyo'))
|
||||||
|
|
||||||
|
# create strings for use in gameSetting
|
||||||
|
reboot_start = reboot_start_time.strftime(self.date_time_format)
|
||||||
|
reboot_end = reboot_end_time.strftime(self.date_time_format)
|
||||||
|
t_port = Utils.get_title_port(self.core_cfg)
|
||||||
return {
|
return {
|
||||||
"gameSetting": {
|
"gameSetting": {
|
||||||
"isMaintenance": False,
|
"isMaintenance": False,
|
||||||
@@ -49,31 +78,31 @@ class ChuniNew(ChuniBase):
|
|||||||
"maxCountMusic": 300,
|
"maxCountMusic": 300,
|
||||||
"matchStartTime": match_start,
|
"matchStartTime": match_start,
|
||||||
"matchEndTime": match_end,
|
"matchEndTime": match_end,
|
||||||
"matchTimeLimit": 60,
|
"matchTimeLimit": self.game_cfg.matching.match_time_limit,
|
||||||
"matchErrorLimit": 9999,
|
"matchErrorLimit": self.game_cfg.matching.match_error_limit,
|
||||||
"romVersion": self.game_cfg.version.version(self.version)["rom"],
|
"romVersion": self.game_cfg.version.version(self.version)["rom"],
|
||||||
"dataVersion": self.game_cfg.version.version(self.version)["data"],
|
"dataVersion": self.game_cfg.version.version(self.version)["data"],
|
||||||
"matchingUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/",
|
"matchingUri": f"http://{self.core_cfg.server.hostname}:{t_port}/SDHD/{self._interal_ver_to_intver()}/ChuniServlet/",
|
||||||
"matchingUriX": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/",
|
"matchingUriX": f"http://{self.core_cfg.server.hostname}:{t_port}/SDHD/{self._interal_ver_to_intver()}/ChuniServlet/",
|
||||||
# might be really important for online battle to connect the cabs via UDP port 50201
|
# might be really important for online battle to connect the cabs via UDP port 50201
|
||||||
"udpHolePunchUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/",
|
"udpHolePunchUri": f"http://{self.core_cfg.server.hostname}:{self.core_cfg.server.port}/SDHD/{self._interal_ver_to_intver()}/ChuniServlet/",
|
||||||
"reflectorUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/",
|
"reflectorUri": f"http://{self.core_cfg.server.hostname}:{self.core_cfg.server.port}/SDHD/{self._interal_ver_to_intver()}/ChuniServlet/",
|
||||||
},
|
},
|
||||||
"isDumpUpload": False,
|
"isDumpUpload": False,
|
||||||
"isAou": False,
|
"isAou": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_remove_token_api_request(self, data: Dict) -> Dict:
|
async def handle_remove_token_api_request(self, data: Dict) -> Dict:
|
||||||
return {"returnCode": "1"}
|
return {"returnCode": "1"}
|
||||||
|
|
||||||
def handle_delete_token_api_request(self, data: Dict) -> Dict:
|
async def handle_delete_token_api_request(self, data: Dict) -> Dict:
|
||||||
return {"returnCode": "1"}
|
return {"returnCode": "1"}
|
||||||
|
|
||||||
def handle_create_token_api_request(self, data: Dict) -> Dict:
|
async def handle_create_token_api_request(self, data: Dict) -> Dict:
|
||||||
return {"returnCode": "1"}
|
return {"returnCode": "1"}
|
||||||
|
|
||||||
def handle_get_user_map_area_api_request(self, data: Dict) -> Dict:
|
async def handle_get_user_map_area_api_request(self, data: Dict) -> Dict:
|
||||||
user_map_areas = self.data.item.get_map_areas(data["userId"])
|
user_map_areas = await self.data.item.get_map_areas(data["userId"])
|
||||||
|
|
||||||
map_areas = []
|
map_areas = []
|
||||||
for map_area in user_map_areas:
|
for map_area in user_map_areas:
|
||||||
@@ -84,14 +113,14 @@ class ChuniNew(ChuniBase):
|
|||||||
|
|
||||||
return {"userId": data["userId"], "userMapAreaList": map_areas}
|
return {"userId": data["userId"], "userMapAreaList": map_areas}
|
||||||
|
|
||||||
def handle_get_user_symbol_chat_setting_api_request(self, data: Dict) -> Dict:
|
async def handle_get_user_symbol_chat_setting_api_request(self, data: Dict) -> Dict:
|
||||||
return {"userId": data["userId"], "symbolCharInfoList": []}
|
return {"userId": data["userId"], "symbolCharInfoList": []}
|
||||||
|
|
||||||
def handle_get_user_preview_api_request(self, data: Dict) -> Dict:
|
async def handle_get_user_preview_api_request(self, data: Dict) -> Dict:
|
||||||
profile = self.data.profile.get_profile_preview(data["userId"], self.version)
|
profile = await self.data.profile.get_profile_preview(data["userId"], self.version)
|
||||||
if profile is None:
|
if profile is None:
|
||||||
return None
|
return None
|
||||||
profile_character = self.data.item.get_character(
|
profile_character = await self.data.item.get_character(
|
||||||
data["userId"], profile["characterId"]
|
data["userId"], profile["characterId"]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -135,8 +164,8 @@ class ChuniNew(ChuniBase):
|
|||||||
}
|
}
|
||||||
return data1
|
return data1
|
||||||
|
|
||||||
def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
|
async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
|
||||||
p = self.data.profile.get_profile_data(data["userId"], self.version)
|
p = await self.data.profile.get_profile_data(data["userId"], self.version)
|
||||||
if p is None:
|
if p is None:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@@ -148,17 +177,17 @@ class ChuniNew(ChuniBase):
|
|||||||
"isLogin": False,
|
"isLogin": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_printer_login_api_request(self, data: Dict) -> Dict:
|
async def handle_printer_login_api_request(self, data: Dict) -> Dict:
|
||||||
return {"returnCode": 1}
|
return {"returnCode": 1}
|
||||||
|
|
||||||
def handle_printer_logout_api_request(self, data: Dict) -> Dict:
|
async def handle_printer_logout_api_request(self, data: Dict) -> Dict:
|
||||||
return {"returnCode": 1}
|
return {"returnCode": 1}
|
||||||
|
|
||||||
def handle_get_game_gacha_api_request(self, data: Dict) -> Dict:
|
async def handle_get_game_gacha_api_request(self, data: Dict) -> Dict:
|
||||||
"""
|
"""
|
||||||
returns all current active banners (gachas)
|
returns all current active banners (gachas)
|
||||||
"""
|
"""
|
||||||
game_gachas = self.data.static.get_gachas(self.version)
|
game_gachas = await self.data.static.get_gachas(self.version)
|
||||||
|
|
||||||
# clean the database rows
|
# clean the database rows
|
||||||
game_gacha_list = []
|
game_gacha_list = []
|
||||||
@@ -184,11 +213,11 @@ class ChuniNew(ChuniBase):
|
|||||||
"registIdList": [],
|
"registIdList": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_game_gacha_card_by_id_api_request(self, data: Dict) -> Dict:
|
async def handle_get_game_gacha_card_by_id_api_request(self, data: Dict) -> Dict:
|
||||||
"""
|
"""
|
||||||
returns all valid cards for a given gachaId
|
returns all valid cards for a given gachaId
|
||||||
"""
|
"""
|
||||||
game_gacha_cards = self.data.static.get_gacha_cards(data["gachaId"])
|
game_gacha_cards = await self.data.static.get_gacha_cards(data["gachaId"])
|
||||||
|
|
||||||
game_gacha_card_list = []
|
game_gacha_card_list = []
|
||||||
for gacha_card in game_gacha_cards:
|
for gacha_card in game_gacha_cards:
|
||||||
@@ -208,8 +237,8 @@ class ChuniNew(ChuniBase):
|
|||||||
"ssrBookCalcList": [],
|
"ssrBookCalcList": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict:
|
async def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict:
|
||||||
p = self.data.profile.get_profile_data(data["userId"], self.version)
|
p = await self.data.profile.get_profile_data(data["userId"], self.version)
|
||||||
if p is None:
|
if p is None:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@@ -233,8 +262,8 @@ class ChuniNew(ChuniBase):
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_gacha_api_request(self, data: Dict) -> Dict:
|
async def handle_get_user_gacha_api_request(self, data: Dict) -> Dict:
|
||||||
user_gachas = self.data.item.get_user_gachas(data["userId"])
|
user_gachas = await self.data.item.get_user_gachas(data["userId"])
|
||||||
if user_gachas is None:
|
if user_gachas is None:
|
||||||
return {"userId": data["userId"], "length": 0, "userGachaList": []}
|
return {"userId": data["userId"], "length": 0, "userGachaList": []}
|
||||||
|
|
||||||
@@ -252,8 +281,8 @@ class ChuniNew(ChuniBase):
|
|||||||
"userGachaList": user_gacha_list,
|
"userGachaList": user_gacha_list,
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_printed_card_api_request(self, data: Dict) -> Dict:
|
async def handle_get_user_printed_card_api_request(self, data: Dict) -> Dict:
|
||||||
user_print_list = self.data.item.get_user_print_states(
|
user_print_list = await self.data.item.get_user_print_states(
|
||||||
data["userId"], has_completed=True
|
data["userId"], has_completed=True
|
||||||
)
|
)
|
||||||
if user_print_list is None:
|
if user_print_list is None:
|
||||||
@@ -287,10 +316,10 @@ class ChuniNew(ChuniBase):
|
|||||||
"userPrintedCardList": print_list,
|
"userPrintedCardList": print_list,
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_card_print_error_api_request(self, data: Dict) -> Dict:
|
async def handle_get_user_card_print_error_api_request(self, data: Dict) -> Dict:
|
||||||
user_id = data["userId"]
|
user_id = data["userId"]
|
||||||
|
|
||||||
user_print_states = self.data.item.get_user_print_states(
|
user_print_states = await self.data.item.get_user_print_states(
|
||||||
user_id, has_completed=False
|
user_id, has_completed=False
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -309,13 +338,13 @@ class ChuniNew(ChuniBase):
|
|||||||
"userCardPrintStateList": card_print_state_list,
|
"userCardPrintStateList": card_print_state_list,
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict:
|
async def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict:
|
||||||
return super().handle_get_user_character_api_request(data)
|
return await super().handle_get_user_character_api_request(data)
|
||||||
|
|
||||||
def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict:
|
async def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict:
|
||||||
return super().handle_get_user_item_api_request(data)
|
return await super().handle_get_user_item_api_request(data)
|
||||||
|
|
||||||
def handle_roll_gacha_api_request(self, data: Dict) -> Dict:
|
async def handle_roll_gacha_api_request(self, data: Dict) -> Dict:
|
||||||
"""
|
"""
|
||||||
Handle a gacha roll API request, with:
|
Handle a gacha roll API request, with:
|
||||||
gachaId: the gachaId where the cards should be pulled from
|
gachaId: the gachaId where the cards should be pulled from
|
||||||
@@ -333,14 +362,14 @@ class ChuniNew(ChuniBase):
|
|||||||
# characterId should be returned
|
# characterId should be returned
|
||||||
if chara_id != -1:
|
if chara_id != -1:
|
||||||
# get the
|
# get the
|
||||||
card = self.data.static.get_gacha_card_by_character(gacha_id, chara_id)
|
card = await self.data.static.get_gacha_card_by_character(gacha_id, chara_id)
|
||||||
|
|
||||||
tmp = card._asdict()
|
tmp = card._asdict()
|
||||||
tmp.pop("id")
|
tmp.pop("id")
|
||||||
|
|
||||||
rolled_cards.append(tmp)
|
rolled_cards.append(tmp)
|
||||||
else:
|
else:
|
||||||
gacha_cards = self.data.static.get_gacha_cards(gacha_id)
|
gacha_cards = await self.data.static.get_gacha_cards(gacha_id)
|
||||||
|
|
||||||
# get the card id for each roll
|
# get the card id for each roll
|
||||||
for _ in range(num_rolls):
|
for _ in range(num_rolls):
|
||||||
@@ -357,7 +386,7 @@ class ChuniNew(ChuniBase):
|
|||||||
|
|
||||||
return {"length": len(rolled_cards), "gameGachaCardList": rolled_cards}
|
return {"length": len(rolled_cards), "gameGachaCardList": rolled_cards}
|
||||||
|
|
||||||
def handle_cm_upsert_user_gacha_api_request(self, data: Dict) -> Dict:
|
async def handle_cm_upsert_user_gacha_api_request(self, data: Dict) -> Dict:
|
||||||
upsert = data["cmUpsertUserGacha"]
|
upsert = data["cmUpsertUserGacha"]
|
||||||
user_id = data["userId"]
|
user_id = data["userId"]
|
||||||
place_id = data["placeId"]
|
place_id = data["placeId"]
|
||||||
@@ -367,7 +396,7 @@ class ChuniNew(ChuniBase):
|
|||||||
user_data.pop("rankUpChallengeResults")
|
user_data.pop("rankUpChallengeResults")
|
||||||
user_data.pop("userEmoney")
|
user_data.pop("userEmoney")
|
||||||
|
|
||||||
self.data.profile.put_profile_data(user_id, self.version, user_data)
|
await self.data.profile.put_profile_data(user_id, self.version, user_data)
|
||||||
|
|
||||||
# save the user gacha
|
# save the user gacha
|
||||||
user_gacha = upsert["userGacha"]
|
user_gacha = upsert["userGacha"]
|
||||||
@@ -375,16 +404,16 @@ class ChuniNew(ChuniBase):
|
|||||||
user_gacha.pop("gachaId")
|
user_gacha.pop("gachaId")
|
||||||
user_gacha.pop("dailyGachaDate")
|
user_gacha.pop("dailyGachaDate")
|
||||||
|
|
||||||
self.data.item.put_user_gacha(user_id, gacha_id, user_gacha)
|
await self.data.item.put_user_gacha(user_id, gacha_id, user_gacha)
|
||||||
|
|
||||||
# save all user items
|
# save all user items
|
||||||
if "userItemList" in upsert:
|
if "userItemList" in upsert:
|
||||||
for item in upsert["userItemList"]:
|
for item in upsert["userItemList"]:
|
||||||
self.data.item.put_item(user_id, item)
|
await self.data.item.put_item(user_id, item)
|
||||||
|
|
||||||
# add every gamegachaCard to database
|
# add every gamegachaCard to database
|
||||||
for card in upsert["gameGachaCardList"]:
|
for card in upsert["gameGachaCardList"]:
|
||||||
self.data.item.put_user_print_state(
|
await self.data.item.put_user_print_state(
|
||||||
user_id,
|
user_id,
|
||||||
hasCompleted=False,
|
hasCompleted=False,
|
||||||
placeId=place_id,
|
placeId=place_id,
|
||||||
@@ -394,7 +423,7 @@ class ChuniNew(ChuniBase):
|
|||||||
|
|
||||||
# retrieve every game gacha card which has been added in order to get
|
# retrieve every game gacha card which has been added in order to get
|
||||||
# the orderId for the next request
|
# the orderId for the next request
|
||||||
user_print_states = self.data.item.get_user_print_states_by_gacha(
|
user_print_states = await self.data.item.get_user_print_states_by_gacha(
|
||||||
user_id, gacha_id, has_completed=False
|
user_id, gacha_id, has_completed=False
|
||||||
)
|
)
|
||||||
card_print_state_list = []
|
card_print_state_list = []
|
||||||
@@ -412,7 +441,7 @@ class ChuniNew(ChuniBase):
|
|||||||
"userCardPrintStateList": card_print_state_list,
|
"userCardPrintStateList": card_print_state_list,
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict:
|
async def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict:
|
||||||
return {
|
return {
|
||||||
"returnCode": 1,
|
"returnCode": 1,
|
||||||
"orderId": 0,
|
"orderId": 0,
|
||||||
@@ -420,7 +449,7 @@ class ChuniNew(ChuniBase):
|
|||||||
"apiName": "CMUpsertUserPrintlogApi",
|
"apiName": "CMUpsertUserPrintlogApi",
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict:
|
async def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict:
|
||||||
user_print_detail = data["userPrintDetail"]
|
user_print_detail = data["userPrintDetail"]
|
||||||
user_id = data["userId"]
|
user_id = data["userId"]
|
||||||
|
|
||||||
@@ -436,7 +465,7 @@ class ChuniNew(ChuniBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# add the entry to the user print table with the random serialId
|
# add the entry to the user print table with the random serialId
|
||||||
self.data.item.put_user_print_detail(user_id, serial_id, user_print_detail)
|
await self.data.item.put_user_print_detail(user_id, serial_id, user_print_detail)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"returnCode": 1,
|
"returnCode": 1,
|
||||||
@@ -445,7 +474,7 @@ class ChuniNew(ChuniBase):
|
|||||||
"apiName": "CMUpsertUserPrintApi",
|
"apiName": "CMUpsertUserPrintApi",
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_cm_upsert_user_print_subtract_api_request(self, data: Dict) -> Dict:
|
async def handle_cm_upsert_user_print_subtract_api_request(self, data: Dict) -> Dict:
|
||||||
upsert = data["userCardPrintState"]
|
upsert = data["userCardPrintState"]
|
||||||
user_id = data["userId"]
|
user_id = data["userId"]
|
||||||
place_id = data["placeId"]
|
place_id = data["placeId"]
|
||||||
@@ -453,37 +482,37 @@ class ChuniNew(ChuniBase):
|
|||||||
# save all user items
|
# save all user items
|
||||||
if "userItemList" in data:
|
if "userItemList" in data:
|
||||||
for item in data["userItemList"]:
|
for item in data["userItemList"]:
|
||||||
self.data.item.put_item(user_id, item)
|
await self.data.item.put_item(user_id, item)
|
||||||
|
|
||||||
# set the card print state to success and use the orderId as the key
|
# set the card print state to success and use the orderId as the key
|
||||||
self.data.item.put_user_print_state(
|
await self.data.item.put_user_print_state(
|
||||||
user_id, id=upsert["orderId"], hasCompleted=True
|
user_id, id=upsert["orderId"], hasCompleted=True
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"returnCode": "1", "apiName": "CMUpsertUserPrintSubtractApi"}
|
return {"returnCode": "1", "apiName": "CMUpsertUserPrintSubtractApi"}
|
||||||
|
|
||||||
def handle_cm_upsert_user_print_cancel_api_request(self, data: Dict) -> Dict:
|
async def handle_cm_upsert_user_print_cancel_api_request(self, data: Dict) -> Dict:
|
||||||
order_ids = data["orderIdList"]
|
order_ids = data["orderIdList"]
|
||||||
user_id = data["userId"]
|
user_id = data["userId"]
|
||||||
|
|
||||||
# set the card print state to success and use the orderId as the key
|
# set the card print state to success and use the orderId as the key
|
||||||
for order_id in order_ids:
|
for order_id in order_ids:
|
||||||
self.data.item.put_user_print_state(user_id, id=order_id, hasCompleted=True)
|
await self.data.item.put_user_print_state(user_id, id=order_id, hasCompleted=True)
|
||||||
|
|
||||||
return {"returnCode": "1", "apiName": "CMUpsertUserPrintCancelApi"}
|
return {"returnCode": "1", "apiName": "CMUpsertUserPrintCancelApi"}
|
||||||
|
|
||||||
def handle_ping_request(self, data: Dict) -> Dict:
|
async def handle_ping_request(self, data: Dict) -> Dict:
|
||||||
# matchmaking ping request
|
# matchmaking ping request
|
||||||
return {"returnCode": "1"}
|
return {"returnCode": "1"}
|
||||||
|
|
||||||
def handle_begin_matching_api_request(self, data: Dict) -> Dict:
|
async def handle_begin_matching_api_request(self, data: Dict) -> Dict:
|
||||||
room_id = 1
|
room_id = 1
|
||||||
# check if there is a free matching room
|
# check if there is a free matching room
|
||||||
matching_room = self.data.item.get_oldest_free_matching(self.version)
|
matching_room = await self.data.item.get_oldest_free_matching(self.version)
|
||||||
|
|
||||||
if matching_room is None:
|
if matching_room is None:
|
||||||
# grab the latest roomId and add 1 for the new room
|
# grab the latest roomId and add 1 for the new room
|
||||||
newest_matching = self.data.item.get_newest_matching(self.version)
|
newest_matching = await self.data.item.get_newest_matching(self.version)
|
||||||
if newest_matching is not None:
|
if newest_matching is not None:
|
||||||
room_id = newest_matching["roomId"] + 1
|
room_id = newest_matching["roomId"] + 1
|
||||||
|
|
||||||
@@ -493,12 +522,12 @@ class ChuniNew(ChuniBase):
|
|||||||
|
|
||||||
# create the new room with room_id and the current user id (host)
|
# create the new room with room_id and the current user id (host)
|
||||||
# user id is required for the countdown later on
|
# user id is required for the countdown later on
|
||||||
self.data.item.put_matching(
|
await self.data.item.put_matching(
|
||||||
self.version, room_id, [new_member], user_id=new_member["userId"]
|
self.version, room_id, [new_member], user_id=new_member["userId"]
|
||||||
)
|
)
|
||||||
|
|
||||||
# get the newly created matching room
|
# get the newly created matching room
|
||||||
matching_room = self.data.item.get_matching(self.version, room_id)
|
matching_room = await self.data.item.get_matching(self.version, room_id)
|
||||||
else:
|
else:
|
||||||
# a room already exists, so just add the new member to it
|
# a room already exists, so just add the new member to it
|
||||||
matching_member_list = matching_room["matchingMemberInfoList"]
|
matching_member_list = matching_room["matchingMemberInfoList"]
|
||||||
@@ -508,7 +537,7 @@ class ChuniNew(ChuniBase):
|
|||||||
matching_member_list.append(new_member)
|
matching_member_list.append(new_member)
|
||||||
|
|
||||||
# add the updated room to the database, make sure to set isFull correctly!
|
# add the updated room to the database, make sure to set isFull correctly!
|
||||||
self.data.item.put_matching(
|
await self.data.item.put_matching(
|
||||||
self.version,
|
self.version,
|
||||||
matching_room["roomId"],
|
matching_room["roomId"],
|
||||||
matching_member_list,
|
matching_member_list,
|
||||||
@@ -525,8 +554,8 @@ class ChuniNew(ChuniBase):
|
|||||||
|
|
||||||
return {"roomId": 1, "matchingWaitState": matching_wait}
|
return {"roomId": 1, "matchingWaitState": matching_wait}
|
||||||
|
|
||||||
def handle_end_matching_api_request(self, data: Dict) -> Dict:
|
async def handle_end_matching_api_request(self, data: Dict) -> Dict:
|
||||||
matching_room = self.data.item.get_matching(self.version, data["roomId"])
|
matching_room = await self.data.item.get_matching(self.version, data["roomId"])
|
||||||
members = matching_room["matchingMemberInfoList"]
|
members = matching_room["matchingMemberInfoList"]
|
||||||
|
|
||||||
# only set the host user to role 1 every other to 0?
|
# only set the host user to role 1 every other to 0?
|
||||||
@@ -535,7 +564,7 @@ class ChuniNew(ChuniBase):
|
|||||||
for m in members
|
for m in members
|
||||||
]
|
]
|
||||||
|
|
||||||
self.data.item.put_matching(
|
await self.data.item.put_matching(
|
||||||
self.version,
|
self.version,
|
||||||
matching_room["roomId"],
|
matching_room["roomId"],
|
||||||
members,
|
members,
|
||||||
@@ -550,13 +579,13 @@ class ChuniNew(ChuniBase):
|
|||||||
# no idea, maybe to differentiate between CPUs and real players?
|
# no idea, maybe to differentiate between CPUs and real players?
|
||||||
"matchingMemberRoleList": role_list,
|
"matchingMemberRoleList": role_list,
|
||||||
# TCP/UDP connection?
|
# TCP/UDP connection?
|
||||||
"reflectorUri": f"{self.core_cfg.title.hostname}",
|
"reflectorUri": f"{self.core_cfg.server.hostname}",
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_remove_matching_member_api_request(self, data: Dict) -> Dict:
|
async def handle_remove_matching_member_api_request(self, data: Dict) -> Dict:
|
||||||
# get all matching rooms, because Chuni only returns the userId
|
# get all matching rooms, because Chuni only returns the userId
|
||||||
# not the actual roomId
|
# not the actual roomId
|
||||||
matching_rooms = self.data.item.get_all_matchings(self.version)
|
matching_rooms = await self.data.item.get_all_matchings(self.version)
|
||||||
if matching_rooms is None:
|
if matching_rooms is None:
|
||||||
return {"returnCode": "1"}
|
return {"returnCode": "1"}
|
||||||
|
|
||||||
@@ -570,10 +599,10 @@ class ChuniNew(ChuniBase):
|
|||||||
|
|
||||||
# if the last user got removed, delete the matching room
|
# if the last user got removed, delete the matching room
|
||||||
if len(new_members) <= 0:
|
if len(new_members) <= 0:
|
||||||
self.data.item.delete_matching(self.version, room["roomId"])
|
await self.data.item.delete_matching(self.version, room["roomId"])
|
||||||
else:
|
else:
|
||||||
# remove the user from the room
|
# remove the user from the room
|
||||||
self.data.item.put_matching(
|
await self.data.item.put_matching(
|
||||||
self.version,
|
self.version,
|
||||||
room["roomId"],
|
room["roomId"],
|
||||||
new_members,
|
new_members,
|
||||||
@@ -583,10 +612,10 @@ class ChuniNew(ChuniBase):
|
|||||||
|
|
||||||
return {"returnCode": "1"}
|
return {"returnCode": "1"}
|
||||||
|
|
||||||
def handle_get_matching_state_api_request(self, data: Dict) -> Dict:
|
async def handle_get_matching_state_api_request(self, data: Dict) -> Dict:
|
||||||
polling_interval = 1
|
polling_interval = 1
|
||||||
# get the current active room
|
# get the current active room
|
||||||
matching_room = self.data.item.get_matching(self.version, data["roomId"])
|
matching_room = await self.data.item.get_matching(self.version, data["roomId"])
|
||||||
members = matching_room["matchingMemberInfoList"]
|
members = matching_room["matchingMemberInfoList"]
|
||||||
rest_sec = matching_room["restMSec"]
|
rest_sec = matching_room["restMSec"]
|
||||||
|
|
||||||
@@ -609,7 +638,7 @@ class ChuniNew(ChuniBase):
|
|||||||
current_member["userName"] = self.read_wtf8(current_member["userName"])
|
current_member["userName"] = self.read_wtf8(current_member["userName"])
|
||||||
members[i] = current_member
|
members[i] = current_member
|
||||||
|
|
||||||
self.data.item.put_matching(
|
await self.data.item.put_matching(
|
||||||
self.version,
|
self.version,
|
||||||
data["roomId"],
|
data["roomId"],
|
||||||
members,
|
members,
|
||||||
@@ -629,4 +658,7 @@ class ChuniNew(ChuniBase):
|
|||||||
"matchingMemberInfoList": [current_member] + diff_members,
|
"matchingMemberInfoList": [current_member] + diff_members,
|
||||||
}
|
}
|
||||||
|
|
||||||
return {"matchingWaitState": matching_wait}
|
return {
|
||||||
|
"roomId": data["roomId"],
|
||||||
|
"matchingWaitState": matching_wait
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,40 +1,18 @@
|
|||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
from core.utils import Utils
|
||||||
from core.config import CoreConfig
|
from core.config import CoreConfig
|
||||||
from titles.chuni.new import ChuniNew
|
from titles.chuni.new import ChuniNew
|
||||||
from titles.chuni.const import ChuniConstants
|
from titles.chuni.const import ChuniConstants
|
||||||
from titles.chuni.config import ChuniConfig
|
from titles.chuni.config import ChuniConfig
|
||||||
|
|
||||||
|
|
||||||
class ChuniNewPlus(ChuniNew):
|
class ChuniNewPlus(ChuniNew):
|
||||||
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
|
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
|
||||||
super().__init__(core_cfg, game_cfg)
|
super().__init__(core_cfg, game_cfg)
|
||||||
self.version = ChuniConstants.VER_CHUNITHM_NEW_PLUS
|
self.version = ChuniConstants.VER_CHUNITHM_NEW_PLUS
|
||||||
|
|
||||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
|
||||||
ret = super().handle_get_game_setting_api_request(data)
|
user_data = await super().handle_cm_get_user_preview_api_request(data)
|
||||||
ret["gameSetting"]["romVersion"] = self.game_cfg.version.version(self.version)[
|
|
||||||
"rom"
|
|
||||||
]
|
|
||||||
ret["gameSetting"]["dataVersion"] = self.game_cfg.version.version(self.version)[
|
|
||||||
"data"
|
|
||||||
]
|
|
||||||
ret["gameSetting"][
|
|
||||||
"matchingUri"
|
|
||||||
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/"
|
|
||||||
ret["gameSetting"][
|
|
||||||
"matchingUriX"
|
|
||||||
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/"
|
|
||||||
ret["gameSetting"][
|
|
||||||
"udpHolePunchUri"
|
|
||||||
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/"
|
|
||||||
ret["gameSetting"][
|
|
||||||
"reflectorUri"
|
|
||||||
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/"
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
|
|
||||||
user_data = super().handle_cm_get_user_preview_api_request(data)
|
|
||||||
|
|
||||||
# hardcode lastDataVersion for CardMaker 1.35 A028
|
# hardcode lastDataVersion for CardMaker 1.35 A028
|
||||||
user_data["lastDataVersion"] = "2.05.00"
|
user_data["lastDataVersion"] = "2.05.00"
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user