290 Commits

Author SHA1 Message Date
Dniel97
70dae79058 Merge branch 'master' into fork_master 2024-03-12 18:33:05 +01:00
Hay1tsme
d09f3e9907 Merge branch 'develop' 2024-01-09 10:34:27 -05:00
Hay1tsme
0c3924a8f0 aimedb: fix lookup with banned or locked access code 2024-01-09 10:33:48 -05:00
zaphkito
4550cb2af5 delete "ADD config config" (#83)
this is because everywhere about docker build write to create and use configs/config folder, not config, if we follow docker installation guide, there will not exist config folder in artemis root. Even if the folder exists, the folder will not be mapped inside the container, then we will get a build error about config not exist
in docker-compose.yml
```
  app:
    hostname: ma.app
    build: .
    volumes:
      - ./aime:/app/aime
      - ./configs/config:/app/config
```

in INSTALL_DOCKER.md

- 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

I checked this file history
in d1259509ad, phantomlan already delete this, but don't know why it come back between 040742a9a4 and 565dc38e9a, maybe git bug? However, now delete it again

Reviewed-on: https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/83
Co-authored-by: zaphkito <zaphkito@noreply.gitea.tendokyu.moe>
Co-committed-by: zaphkito <zaphkito@noreply.gitea.tendokyu.moe>
2024-01-09 15:31:03 +00:00
Kevin Trocolli
add85aeab7 Merge branch 'develop' 2024-01-08 21:26:26 -05:00
Kevin Trocolli
b056ff218d Update changelog 2024-01-08 21:25:45 -05:00
Kevin Trocolli
01591a0456 chuni: revert changes to udpHolePunchUri and reflectorUri 2024-01-08 21:23:10 -05:00
Kevin Trocolli
b4b40869c1 chuni: add roomId to handle_get_matching_state_api_request, update changelog 2024-01-08 20:21:27 -05:00
Hay1tsme
c9dfbc77c4 Merge pull request 'Fix issue in handle_get_user_music_api_request for Chunithm where songs would not always return all scores' (#92) from Kumubou/artemis:develop into develop
Reviewed-on: https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/92
2024-01-09 01:20:46 +00:00
Hay1tsme
1b3e43b918 chuni: Imrpove GetGameSetting request handling for different versions 2024-01-08 18:30:03 -05:00
Hay1tsme
f5c77f04fa update changelog 2024-01-08 18:23:15 -05:00
Hay1tsme
27bf51f9f8 chuni: add matching config, stun turn stuff 2024-01-08 18:22:09 -05:00
Hay1tsme
343424e26a update changelog 2024-01-08 18:00:06 -05:00
Kumubou
5f33b2d3e4 Fix issue in handle_get_user_music_api_request where songs would not always return all scores 2024-01-03 22:05:24 +00:00
Midorica
19baf05d7b cxb: fixing get_allnet_info 2023-12-29 13:59:53 -05:00
Midorica
32419faf01 Update docs/game_specific_info.md
Removing unused option details for cxb
2023-12-29 03:20:44 +00:00
Midorica
bf7d709b49 chuni: fixing the network encryption for Sun Plus 2023-12-15 22:22:36 -05:00
Midorica
37b1f41b44 fixing the upgrade & downgrade script for chunithm 2023-12-15 22:21:05 -05:00
Midorica
7b8611cee3 Merge pull request 'Chunithm Sun Plus, Int support' (#78) from EmmyHeart/artemis-sp:develop into develop
Reviewed-on: https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/78
2023-12-16 03:20:02 +00:00
EmmyHeart
d0f8568e17 Fixed Rival Music API not returning everything due to iteration issues 2023-12-13 23:28:00 +00:00
EmmyHeart
fd0fefa48b Added Sun Plus to example config
my bad
2023-12-13 12:28:28 +00:00
EmmyHeart
b5462276f5 Last remnant. Goodbye, rank scaling. 2023-12-13 07:35:07 +00:00
EmmyHeart
626ea58f75 Updated game specific info for Sun Plus
Also removed some old info about rank scaling, since it got YEETed
2023-12-13 07:34:37 +00:00
EmmyHeart
40e6e42c87 Updated readme.md 2023-12-13 07:33:01 +00:00
EmmyHeart
a2d238857c Fixed CM compatibility with new Chuni URL structure 2023-12-13 06:37:30 +00:00
EmmyHeart
a6b315185d Incremented schema version 2023-12-13 06:35:55 +00:00
EmmyHeart
9b9d1f664d Rollback for Sun Plus changed
should hopefully never need to use this
2023-12-13 06:34:32 +00:00
EmmyHeart
b6d1f2cd3a Updated playlog and best tables for Sun Plus
Lord have mercy, this is my first schema update
2023-12-13 06:33:06 +00:00
EmmyHeart
0060122613 Recognize Chunithm Int code 2023-12-13 06:09:02 +00:00
EmmyHeart
97963adf38 Updated matchnaming URLs to use game codes 2023-12-13 06:07:40 +00:00
EmmyHeart
1bfe3584b1 Updated matchmaking URLs to reflect game codes 2023-12-13 06:06:16 +00:00
EmmyHeart
ec4ae98beb Updated matchnaming URLs to utilize game codes 2023-12-13 06:05:46 +00:00
EmmyHeart
a0dc8421eb Updated schema for changes in Sun Plus
isClear and isSuccess are now integers to represent different clear types, although more research is needed into what those types mean.
2023-12-13 06:04:21 +00:00
EmmyHeart
ed731e7407 Added Sun Plus support 2023-12-13 06:02:32 +00:00
EmmyHeart
15204f8d8a Added Sun Plus support, and Int workaround
Int versions of Chunithm use nearly the same endpoints, just with C3Exp at the end. We can treat them the same as the regular versions of the game for now by simply removing the C3Exp portion of the endpoints and running our current logic. Note that later we should treat Int and JP as separate versions
2023-12-13 06:01:31 +00:00
EmmyHeart
32362dbe1e Added const support for both International and Sun Plus 2023-12-13 05:58:22 +00:00
EmmyHeart
59db7ad44a Fixed a null condition when importing from Aqua 2023-12-13 05:56:40 +00:00
Kevin Trocolli
5491266a85 sao: fix https 2023-12-10 18:35:59 -05:00
Kevin Trocolli
283cf41bce chuni: bandaid fix for if a user or team is named 'true' or 'false', fixes #65 2023-12-10 17:41:05 -05:00
Kevin Trocolli
26cdc6c10f maidx: ignore guest upserts, fixes #74 2023-12-10 17:36:29 -05:00
Kevin Trocolli
c1412ca9a8 sao: fix title server port 2023-12-10 17:27:50 -05:00
Hay1tsme
21492fbfb3 wacca: hotfix handle_housing_start_request 2023-12-07 18:53:51 -05:00
Hay1tsme
82c5ae3ad7 wacca: fix housing/start on lilyr and above 2023-12-07 16:07:17 -05:00
Hay1tsme
262155f83f ongeki: fix handle_upsert_client_setting_api_request 2023-12-04 11:40:20 -05:00
Hay1tsme
936316f129 ongeki: fix put_client_setting_data 2023-12-04 11:34:58 -05:00
Kevin Trocolli
104a483f4c maimai: fixes for finale 2023-12-02 22:30:55 -05:00
Hay1tsme
29a1dac983 Merge pull request 'Fixed typo in game_specific_info.md' (#73) from AkaiiKitsune/artemis:akaiikitsune-typo into develop
Reviewed-on: https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/73
2023-12-03 02:13:56 +00:00
Kevin Trocolli
8c0ebbd21b mai: fix pre-dx 2023-12-02 21:01:55 -05:00
Kevin Trocolli
d024b2eeb8 billing: fixes 2023-12-02 21:01:33 -05:00
Hay1tsme
ed2d306dee adb: add configurable auth id lifetime 2023-11-30 18:22:01 -05:00
Hay1tsme
ffc0f2fa5e add keychip ID to auth key 2023-11-30 18:17:40 -05:00
Hay1tsme
a1569bca9d fix typing for create_sega_auth_key 2023-11-30 18:14:20 -05:00
Hay1tsme
cc86d4509e adb: fix for when user_id is None 2023-11-30 18:10:22 -05:00
Kevin Trocolli
216fa49f8b aimedb: implement auth keys 2023-11-30 02:07:24 -05:00
Kevin Trocolli
662fd05d24 ongeki: bump schema ver, fix error if events lookup fails 2023-11-29 18:56:57 -05:00
Hay1tsme
46f61325cb aimedb: soft impl of auth ids 2023-11-29 18:01:19 -05:00
Hay1tsme
e7fb9ce07d mai2: fix get_allnet_info 2023-11-29 11:39:19 -05:00
Farewell_
fbc0e489bc Fixed typo in game_specific_info.md
I suck at git; The Maimai version table has been fixed as suggested.
2023-11-29 11:34:15 +00:00
Hay1tsme
6774716e06 add sega auth key encode/decode in Utils, requires pyjwt 2023-11-27 16:39:50 -05:00
Midorica
2efdf79b87 Merge pull request 'fixup technical challenge event' (#71) from phantomlan/artemis:develop into develop
Reviewed-on: https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/71
2023-11-25 23:24:00 +00:00
phantomlan
cf0c34cafb fixup technical challenge event 2023-11-26 00:12:23 +01:00
Midorica
5fd966eaad cxb: adding non-omni music list support 2023-11-25 16:03:26 -05:00
Midorica
cdd67ffbc3 cxb: fix data handlers for s1 & add coupons for s2 2023-11-25 15:41:38 -05:00
Midorica
7fb98118b2 cxb: fixing index and rss2 /data handler 2023-11-25 13:23:39 -05:00
Kevin Trocolli
fe25359e8e chuni: add userRecentPlayerList check in upsertuserall 2023-11-25 01:00:49 -05:00
Kevin Trocolli
468c99c3c2 add gitattributes to hopefully stop git from clowing our CSV, TXT and JSON data files 2023-11-25 00:43:48 -05:00
Kevin Trocolli
6504f120ad chuni: bandaid fixes for air 2023-11-25 00:31:15 -05:00
Kevin Trocolli
d769285d89 Merge branch 'develop' of https://gitea.tendokyu.moe/Hay1tsme/artemis into develop 2023-11-24 10:36:47 -05:00
Kevin Trocolli
ad61bb3d9b mai2: fis issue with saving and loading charges 2023-11-24 10:36:46 -05:00
Hay1tsme
e941c6a606 add host field to allnet response for some games to fix an issue with nginx 2023-11-20 11:54:13 -05:00
Kevin Trocolli
8bd361d3e3 wacca: fix some logging 2023-11-19 12:15:57 -05:00
Kevin Trocolli
0eee2e92a8 wacca: fix bad items being given 2023-11-19 11:43:46 -05:00
Kevin Trocolli
b4a0d331d4 wacca: fix housing/start for unregistered serials 2023-11-19 11:28:42 -05:00
Kevin Trocolli
d467070ba7 wacca: fix handle_user_status_create_request for lily 2023-11-19 01:18:09 -05:00
Kevin Trocolli
e7f35db3a4 fix comparability with python 8.7 - 3.10 2023-11-19 01:05:34 -05:00
Kevin Trocolli
0422a2bfd4 wacca: fix lily using wrong handler for hosuing/start 2023-11-19 01:05:16 -05:00
Hay1tsme
05be10dabc wacca: add request validation, fix HousingStartRequestV2 2023-11-17 11:37:11 -05:00
Hay1tsme
9a017f75e9 wacca: add traceback printing 2023-11-17 10:57:12 -05:00
Hay1tsme
69cd79003c wacca: fix exception handling 2023-11-17 10:45:18 -05:00
Midorica
d3b01e03c5 Merge pull request 'Hotfix for mission events' (#68) from phantomlan/artemis:develop into develop
Reviewed-on: https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/68
2023-11-15 14:30:33 +00:00
phantomlan
a7e0099294 Merge branch 'develop' of https://gitea.tendokyu.moe/phantomlan/artemis into develop 2023-11-15 15:14:23 +01:00
phantomlan
dcebc5c21a update item.py put_mission_point 2023-11-15 15:11:39 +01:00
phantomlan
04c708be8c update item.py put_mission_point 2023-11-15 15:09:57 +01:00
Kevin Trocolli
67c9ac5ef9 sao: noop get_m_res_earn_campaign_shops 2023-11-15 00:46:51 -05:00
Hay1tsme
328f64ad8a sao: hotfix 2023-11-14 22:11:24 -05:00
Hay1tsme
e0aeb2ac21 sao: fix temp responses 2023-11-14 22:08:09 -05:00
Hay1tsme
3d8ffaa098 sao: fix csc fields not being integers 2023-11-14 21:24:33 -05:00
Hay1tsme
7eb095cdb4 soa: fix get_m_gasha_medal_shops 2023-11-14 21:18:22 -05:00
Hay1tsme
9c49a4f54a sao: fix get_m_res_earn_campaign_shops 2023-11-14 21:07:58 -05:00
Hay1tsme
dffa11f420 sao: fix get_m_gasha_medal_shops 2023-11-14 20:51:51 -05:00
Kevin Trocolli
1ba14da90c sao: gasha medal shop handler classes 2023-11-14 13:20:05 -05:00
Hay1tsme
0003ca4412 sao: fix bad handlers 2023-11-13 17:39:45 -05:00
Hay1tsme
0ca041c042 sao: fix date formatting in YuiMedalShopData 2023-11-13 17:36:22 -05:00
Hay1tsme
25528203fe sao: fix encoder helpers 2023-11-13 17:31:23 -05:00
Hay1tsme
a06d1aaf8c sao: fix helpers 2023-11-13 17:24:22 -05:00
Hay1tsme
e035806b41 sao: fix yui shops 2023-11-13 17:17:27 -05:00
Hay1tsme
a9f72cec69 sao: add shop data tables 2023-11-13 14:45:20 -05:00
Hay1tsme
22cf3f83a6 remove randbytes 2023-11-13 13:47:17 -05:00
Hay1tsme
ee45c55f66 sao: stub get_shop_resource_sales_data_list 2023-11-13 12:53:40 -05:00
Hay1tsme
2c275d1130 idac: add logging to setup 2023-11-13 12:14:34 -05:00
Midorica
b19288fb7e idac: removing public urls 2023-11-13 12:06:14 -05:00
Midorica
a83edee657 Merge pull request 'Initial D THE ARCADE support added' (#41) from Dniel97/artemis:idac into develop
Reviewed-on: https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/41
2023-11-13 16:35:53 +00:00
Dniel97
d1a7b898a7 idac: hotfix for title port and changelog 2023-11-13 17:32:39 +01:00
Dniel97
e561f8f15d idac: updated to latest develop branch, minified timeRelease json 2023-11-13 17:27:14 +01:00
Midorica
c9a20b2433 ongeki: fixing render_POST 2023-11-13 10:47:39 -05:00
Midorica
1034a505f0 fixing get_allnet_info for cxb, sao and ongeki 2023-11-13 10:43:38 -05:00
Dniel97
565dc38e9a Merge branch 'develop' into idac 2023-11-13 16:18:04 +01:00
Midorica
1a4dc88c8a cxb: cleanup of config 2023-11-13 10:12:52 -05:00
Midorica
005adfae38 Merge pull request 'Add Support for Missing API Calls in ONGEKI' (#61) from phantomlan/artemis:develop into develop
Reviewed-on: https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/61
2023-11-13 03:15:47 +00:00
phantomlan
80ae372ccf fix rollback 2023-11-13 04:05:13 +01:00
phantomlan
e461ffe587 add more info 2023-11-13 03:38:30 +01:00
phantomlan
b56a5f020b add rollback sql
move GP support to table in DB
small fixes
2023-11-13 03:26:09 +01:00
phantomlan
d663b1ef7c fix foreign key issue 2023-11-13 03:03:04 +01:00
phantomlan
31d39ffb37 merge conflict fix 2023-11-13 02:01:56 +00:00
phantomlan
530016ef84 i ate one ) 2023-11-13 02:01:56 +00:00
phantomlan
f81c53558e - Add ClientTestmode upsert
- Add ClientSetting upsert
- Add endDate for Events
- Add Upgrade Schema
- Small bugfixes for events
2023-11-13 02:01:56 +00:00
phantomlan
d2e2c14074 update Event Ranking, Tech Challenge, and Music Ranking 2023-11-13 02:01:56 +00:00
phantomlan
1897e8002d add RewardList support
add PresentList support
add reading rewards to read.py
add Ranking Music List support
2023-11-13 02:01:56 +00:00
phantomlan
4da886a083 - Add Ranking Event Support
- Add Technical Challenge Event Support
- Fix Event Enumeration for EVT_TYPES as Enum starts with 1, to be in spec with what game expects, also add missing Max EVT_TYPE
- Add documentation on how to properly configure and run Events for ONGEKI
2023-11-13 02:01:56 +00:00
Kevin Trocolli
4bedf71d3d sao: fix trial_tower_play_end 2023-11-12 20:02:08 -05:00
Kevin Trocolli
84b44d2a02 sao: add logging to synthesize_enhancement_equipment 2023-11-12 19:55:10 -05:00
Kevin Trocolli
b516da2e6e sao: add logging to synthesize_enhancement_hero_log 2023-11-12 19:53:36 -05:00
Kevin Trocolli
a767646361 sao: make synthesize_enhancement_hero_log similar to synthesize_enhancement_equipment 2023-11-12 19:48:03 -05:00
Kevin Trocolli
3596f0f34c sao: fix synthesize_enhancement_equipment 2023-11-12 19:45:39 -05:00
Kevin Trocolli
ab3b6a9814 sao: fix SaoSynthesizeEnhancementHeroLogRequest 2023-11-12 19:24:08 -05:00
Kevin Trocolli
c384a8d1f5 sao: properly initialize material_common_reward_user_data_count 2023-11-12 19:22:46 -05:00
Kevin Trocolli
9bc76279f1 sao: fix episode_play_end 2023-11-12 18:46:38 -05:00
Kevin Trocolli
9026c25fcc sao: fix both unanalyzed log fixed requests 2023-11-12 17:35:06 -05:00
Kevin Trocolli
ff48438b85 sao: fix change_party 2023-11-12 17:26:21 -05:00
Midorica
e1a70d1a06 sao: fix dict lengths of multiple response handlers 2023-11-12 16:13:04 -05:00
Midorica
1ff0e4e2c6 sao: fix episode_play_end again 2023-11-11 11:10:57 -05:00
Kevin Trocolli
c23ab1438e sao: remove unused imports 2023-11-11 01:13:09 -05:00
Kevin Trocolli
773729dc0c sao: fix episode_play_end 2023-11-11 01:11:41 -05:00
Midorica
0e739b2077 adding SSL support for SAO 2023-11-10 23:20:30 -05:00
Midorica
c4f2232457 fixing threading profile loading on CXB 2023-11-10 20:32:24 -05:00
Hay1tsme
9fdd96b717 sao: fix synthesize_enhancement_equipment 2023-11-10 18:01:16 -05:00
Hay1tsme
c370542761 sao: fix req header 2023-11-10 15:52:55 -05:00
Kevin Trocolli
a299ba98ac sao: fix erronius length errors 2023-11-09 23:37:40 -05:00
Kevin Trocolli
44755d4d0f sao: begin implementing request helpers 2023-11-09 23:29:06 -05:00
Kevin Trocolli
3d62b9d372 sao: add SaoResponseHeader 2023-11-09 22:36:33 -05:00
Kevin Trocolli
18bf250fd7 sao: fix length calculation 2023-11-09 21:23:19 -05:00
Kevin Trocolli
0fa8fe06f0 sao: fix crypt maybe 2023-11-09 21:17:18 -05:00
Kevin Trocolli
4cd1181ef7 sao: fix generic handler 2023-11-09 21:09:55 -05:00
Kevin Trocolli
8d190ce7f0 sao: fix padding 2023-11-09 21:05:30 -05:00
Kevin Trocolli
81d588cbc7 sao: crypt fixes 2023-11-09 21:00:51 -05:00
Kevin Trocolli
e6801c1c46 sao: add logging 2023-11-09 20:56:33 -05:00
Kevin Trocolli
840db275fa sao: add encrtped response 2023-11-09 20:56:03 -05:00
Midorica
8769c99f4a fixing ongeki handler 2023-11-09 19:34:04 -05:00
Hay1tsme
3c06f46644 sao: add decoder helpers, hash checks 2023-11-09 18:11:58 -05:00
Hay1tsme
95234a421c sao: fix header 2023-11-09 13:47:50 -05:00
Hay1tsme
c425ca1ea8 sao: hotfix 2023-11-09 13:41:56 -05:00
Hay1tsme
b0ebcaf0cf sao: parse header. add crypt 2023-11-09 13:16:32 -05:00
Hay1tsme
8560c05928 sao: remove hostname/port config, fix allnet uri 2023-11-09 11:55:49 -05:00
Hay1tsme
580a9faae8 sao: fix allnet 2023-11-09 11:51:50 -05:00
Hay1tsme
9537331d1c sao: fix endpoint 2023-11-09 11:32:04 -05:00
Midorica
4047a38370 adding encryption version check for Ongeki 2023-11-09 00:10:19 -05:00
Midorica
eaf64dbd50 Merge branch 'develop' of https://gitea.tendokyu.moe/Hay1tsme/artemis into develop 2023-11-09 00:06:24 -05:00
Midorica
e769404e16 fixing TLS support for ongeki 2023-11-09 00:06:15 -05:00
Kevin Trocolli
eb10bc2560 idz: fix news url 2023-11-08 23:54:35 -05:00
Midorica
4b22bd734e fixing handle_data for Crossbeats 2023-11-08 23:36:26 -05:00
Hay1tsme
94c326a27d ongeki: add option to use https for red and beyond 2023-11-08 21:42:53 -05:00
Kevin Trocolli
cb8eaae2c0 Per-version URI/Host (#66)
Allows setting allnet uri/host response based on things like version, config files, and other factors to accommodate a wider range of potential setups under the same roof. This DOES require all titles to adopt a new structure but it's documented and should hopefully be somewhat intuitive.

Co-authored-by: Hay1tsme <kevin@hay1ts.me>
Reviewed-on: https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/66
Co-authored-by: Kevin Trocolli <pitok236@gmail.com>
Co-committed-by: Kevin Trocolli <pitok236@gmail.com>
2023-11-09 02:17:48 +00:00
Midorica
c2a330f42c Merge pull request 'fix: dockerfile fixes' (#60) from Rylie/artemis:fix/docker into develop
Reviewed-on: https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/60
2023-11-08 20:53:00 +00:00
Midorica
a497a9806d fixing CXB render_POST 2023-11-07 22:40:15 -05:00
Midorica
fbd1d7cb1c fixing CXB render_POST 2023-11-06 23:35:02 -05:00
Midorica
e88e1f82f8 fixing get_energy for CXB 2023-11-06 21:55:05 -05:00
Midorica
fbd12fd6e2 fixing get_energy for CXB 2023-11-06 21:54:06 -05:00
Midorica
1e84a50330 fixing again the render_POST for CXB 2023-11-06 21:04:49 -05:00
Midorica
40e6c018e9 fixing again the render_POST for CXB 2023-11-06 21:04:13 -05:00
Dniel97
4c3852d6d4 IDAC: Added round event "Season 2 2nd" 2023-11-06 19:54:13 +01:00
Dniel97
acac2c4670 IDAC: time trial should display car time now 2023-11-06 18:11:30 +01:00
UncleJim
1224baeade Experimental online match API support and car use countting fix (#5)
Reviewed-on: https://gitea.tendokyu.moe/Dniel97/artemis/pulls/5
Co-authored-by: UncleJim <unclejim@noreply.gitea.tendokyu.moe>
Co-committed-by: UncleJim <unclejim@noreply.gitea.tendokyu.moe>
2023-11-06 15:32:40 +00:00
Rylie
040742a9a4 fix(docker): rename log to logs for consistency 2023-11-05 11:10:01 +07:00
Rylie
b4a0736f7b fix(docker): add pkg-config for docker build process 2023-11-05 11:09:52 +07:00
Kevin Trocolli
a7b0b1be49 allnet: fix dli report logging 2023-11-04 12:41:47 -04:00
Kevin Trocolli
dde397a96c allnet: fix event type for handle_dlorder_report 2023-11-01 20:09:14 -04:00
Kevin Trocolli
f99bf7d1ed allnet: add DLI_STATUS 2023-11-01 20:08:20 -04:00
Kevin Trocolli
bd3e1918aa allnet: further dl report logging 2023-11-01 01:17:23 -04:00
Kevin Trocolli
ffc0a07f6d allnet: fix dl report message typo 2023-11-01 01:02:09 -04:00
Kevin Trocolli
ab640760a9 allnet: add event logging to download report 2023-11-01 00:53:04 -04:00
Midorica
a8f06ee266 Merge pull request 'Aime Locks/Bans and Chunithm Improvements' (#47) from EmmyHeart/artemis:develop into develop
Reviewed-on: https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/47
2023-10-27 17:25:35 +00:00
UncleJim
3f192bd84a Proper store name and region handling (#4)
Now the code will read store name from `aime.arcade` table instead of putting `self.core_cfg.server.name` into it braindead-ly

Reviewed-on: https://gitea.tendokyu.moe/Dniel97/artemis/pulls/4
Co-authored-by: UncleJim <unclejim@noreply.gitea.tendokyu.moe>
Co-committed-by: UncleJim <unclejim@noreply.gitea.tendokyu.moe>
2023-10-27 11:05:55 +00:00
Hay1tsme
4b83d3f316 Merge pull request 'wacca: name unknown fields, rename incorrect fields.' (#56) from Yellowberry/artemis:wacca_notice_fix into develop
Reviewed-on: https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/56
2023-10-27 02:06:38 +00:00
Midorica
1b5c335f4e Merge pull request 'Chunithm: Fix getUserMusic duplicate songs and fix missing song diffs' (#54) from DSRLIN/artemis-bugfix:develop into develop
Reviewed-on: https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/54
2023-10-27 01:51:43 +00:00
Zsolt Zitting
680223dba2 wacca: name unknown fields, rename incorrect fields. 2023-10-26 14:25:04 -07:00
UncleJim
4fc4d98a74 Always return matching state to keep players out of looping back to matching screen (#6)
Reviewed-on: https://gitea.tendokyu.moe/Dniel97/artemis/pulls/6
Co-authored-by: UncleJim <unclejim@noreply.gitea.tendokyu.moe>
Co-committed-by: UncleJim <unclejim@noreply.gitea.tendokyu.moe>
2023-10-26 10:21:01 +00:00
DSRLIN
3259552c29 update bug 2023-10-26 05:38:33 +00:00
DSRLIN
b5ce13d1f1 fix chuni songlist duplicate songs and missing diff 2023-10-26 05:37:11 +00:00
Midorica
be86448b23 Merge pull request 'Updates and Fixes for Dockerfile and docker-compose, and docker installation guide' (#51) from phantomlan/artemis:develop into develop
Reviewed-on: https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/51
2023-10-22 14:22:41 +00:00
phantomlan
01182087e0 Merge branch 'docker-and-docs' into develop 2023-10-21 20:38:14 +02:00
phantomlan
e9637c94de update dx.py 2023-10-21 20:35:46 +02:00
phantomlan
37a6502dc9 Merge branch 'docker-and-docs' into develop 2023-10-21 20:04:16 +02:00
phantomlan
d1259509ad update pull request 2023-10-21 19:57:45 +02:00
phantomlan
7f1ff60b9d Update docs/INSTALL_DOCKER.md 2023-10-21 15:54:17 +00:00
phantomlan
12d0a3f927 - prettier docker guide
- update docker-compose to reflect 1 level higher config folder
2023-10-21 17:47:23 +02:00
phantomlan
5420076c8e - update docker-compose database charset and collation to utf8mb4
- update TODO list in docker guide
- update readme.md
2023-10-21 17:33:58 +02:00
phantomlan
a3d2955fce - update docker-compose.yml
- update mysqldb to mariadb-alpine
- update mysql healthcheck to also include Password
- update memcached to alpine counterpart
- update phpmyadmin port to not collide with ARTEMiS main app
- add commented out options for DB Persistency

- update Dockerfile
- add read.py to Dockerfile

- add docs/INSTALL_DOCKER.md guide
2023-10-21 17:22:58 +02:00
EmmyHeart
990d60cf27 Forgot to remove rank scale option from the example config, as it does literally nothing now lol 2023-10-20 04:25:08 +00:00
EmmyHeart
21cb37001b Stubbed Team Course functions as they do not currently do anything 2023-10-20 03:31:36 +00:00
EmmyHeart
32903d979e Added code for song rankings using romVersion, and ensuring romVersion is set on playlog upsert 2023-10-20 03:29:05 +00:00
EmmyHeart
3f8c62044c Optimized rival music list, added ranking API, and began work on Team Courses (need help) 2023-10-20 03:26:51 +00:00
EmmyHeart
719ae9cfb1 Added checks for if Aime card is locked or banned, and pass that status to the game 2023-10-20 03:25:12 +00:00
EmmyHeart
8fcd227b33 Added functions for checking if Aime card is locked or banned 2023-10-20 03:24:25 +00:00
Dniel97
b5e3bf6f80 IDAC: Hotfix for UTF-8 encoding 2023-10-19 19:04:06 +02:00
Dniel97
81f7106264 IDAC: avatar gacha updated (HUGE PAIN), config fixes 2023-10-19 18:52:10 +02:00
Midorica
54c77c047e fixing duplicated is_cab value from machine table 2023-10-18 18:44:03 -04:00
Midorica
93b8b86b55 Adding base handler & config for HTTPS title support 2023-10-17 13:12:08 -04:00
Midorica
eaa2652647 Merge pull request 'Chunithm Fixes and Maintenance for all!' (#46) from EmmyHeart/artemis:develop into develop
Reviewed-on: https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/46
2023-10-17 17:00:08 +00:00
Midorica
46f83b9925 Merge pull request 'maimai DX FESTiVAL PLUS support' (#44) from Dniel97/artemis:mai2_festival_plus into develop
Reviewed-on: https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/44
2023-10-16 20:00:08 +00:00
Dniel97
862907b82a mai2: Fixed cards not showing up 2023-10-16 21:36:26 +02:00
EmmyHeart
8c114532e8 Updated config documentation with IP enforcement and reboot times 2023-10-16 13:30:10 +00:00
EmmyHeart
300cd10abf Updated example config with reboot time
You can leave this option out to disable reboot times, and the games will use the prior mechanism of maintenance always being 4~ hours before current time
2023-10-16 13:23:56 +00:00
EmmyHeart
4f0a5f60ab Added config for reboot time support 2023-10-16 13:22:18 +00:00
EmmyHeart
9cff321857 Added reboot time support 2023-10-16 13:21:07 +00:00
EmmyHeart
8d289ca066 Added reboot time support 2023-10-16 13:20:37 +00:00
EmmyHeart
e18b87ee5c Added reboot time support 2023-10-16 13:20:00 +00:00
EmmyHeart
540d7fc2c2 Added reboot time support 2023-10-16 13:18:23 +00:00
EmmyHeart
3ef40fe85e Removed Team Rank Scaling and fixed RIval Music Data. Also cleaned up extraneous functions 2023-10-16 13:15:39 +00:00
EmmyHeart
35a7525f74 Removed extraneous function 2023-10-16 13:14:11 +00:00
EmmyHeart
d49997f832 Team Rank Scaling is dead, long live Team Rank Scaling 2023-10-16 13:09:53 +00:00
EmmyHeart
c78a62de14 Fixed Rival Music retrieval 2023-10-16 13:09:11 +00:00
EmmyHeart
480551f942 Fixed logic error leading to strict IP checking always being enabled 2023-10-16 13:07:05 +00:00
Dniel97
d6e4db48f4 Added maimai DX FESTiVAL PLUS support
- Added Card Maker support for FESTiVAL PLUS
- Bumped SDEZ database to version 8
- Updated docs for FESTiVAL PLUS
2023-10-15 19:04:15 +02:00
Dniel97
346b74e51b Merge branch 'develop' into fork_develop 2023-10-15 18:06:07 +02:00
Midorica
aa9d48ccc9 fixed the diva profile again 2023-10-10 18:37:18 -04:00
Midorica
dc02d60690 Merge branch 'develop' of https://gitea.tendokyu.moe/Hay1tsme/artemis into develop 2023-10-10 18:25:01 -04:00
Midorica
6d592dcbc7 fixing skin issue with diva profile 2023-10-10 18:24:54 -04:00
Dniel97
3ee1801ee6 IDAC: Hotfix for play stamp and time trials 2023-10-10 17:58:20 +02:00
Dniel97
31188ccce6 IDAC: Hotfix to block amdaemon_emu users without correct access code 2023-10-10 16:06:58 +02:00
Midorica
ab10f27cb4 Merge pull request 'Fixed typo with game_cfg/game_config' (#42) from EmmyHeart/artemis:develop into develop
Reviewed-on: https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/42
2023-10-09 12:38:46 +00:00
EmmyHeart
0a12e93593 news_msg config option doesn't actually return anything. Fixed! 2023-10-09 06:47:55 +00:00
EmmyHeart
bad106ceba Fixed typo with game_cfg/game_config
This resulted in an exception on Plost and earlier, leading to games not actually working.
2023-10-09 06:35:14 +00:00
Midorica
7fc5544c15 adding a SAO note to documentation 2023-10-08 15:52:41 -04:00
Dniel97
d55ada2538 IDAC: Improvements
- Fixed play stamp event name not shown
- Fixed broken cars such as: LEVIN SR (AE85), SKYLINE 25GT TURBO (ER34), S2000 (AP1), GR SUPRA (DB42), SWIFT Sport (ZC33S), LANCER RS EVOLUTION Ⅴ (CP9A), LANCER GSR EVOLUTION Ⅵ TMEDITION (CP9A)
- Fixed bunta/special mode not properly updating
- Fixed frontend (for real this time)
2023-10-08 19:56:04 +02:00
Midorica
bfaadff9f6 pushing news support customization for chunithm 2023-10-07 12:31:08 -04:00
Midorica
1996f3f356 fix for chunithm song loading after 600 scores 2023-10-07 12:11:24 -04:00
Midorica
56ddd3b1cc fix yet again to the md file 2023-10-07 12:07:14 -04:00
Midorica
41fcadfd55 quick fix to markdown file 2023-10-07 12:04:33 -04:00
Midorica
06d95c8c5f Adding details under the game specific info doc 2023-10-07 11:59:01 -04:00
Kevin Trocolli
b5ccd67940 db: add memcache toggle 2023-10-05 22:16:50 -04:00
Midorica
1f65cfd2eb Merge pull request 'Added Team and Rival support to Chunithm' (#24) from EmmyHeart/artemis:develop into develop
Reviewed-on: https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/24
2023-10-06 01:48:50 +00:00
EmmyHeart
9681f86e33 Chunithm SQL documentation
Figured I would outline all of the neat SQL tricks you can do with this build, as well as properly document the features.
2023-10-05 07:56:46 +00:00
Dniel97
8ccb7f08f2 IDAC: Fixed frontend user_id not found 2023-10-03 17:00:59 +02:00
Dniel97
0e664e03f9 IDAC: Updated docs, added TA Data
Known issue: after selecting Season 1, the TA Data for Season 2 will not be shown
2023-10-03 16:50:52 +02:00
Hay1tsme
d641705273 pokken: fix last_play_event_id 2023-10-02 15:12:29 -04:00
Kevin Trocolli
3d13eb1698 pokken: add coalesce 2023-10-01 23:49:47 -04:00
Kevin Trocolli
33e0288e5e pokken: pokemon data save/load 2023-10-01 22:38:56 -04:00
Dniel97
ee5f13a3bb IDAC: Added changelog, fixed doc 2023-10-01 22:29:50 +02:00
Dniel97
4ba01cdf20 IDAC: Fixed time trial events config, added new events 2023-10-01 22:17:06 +02:00
Dniel97
7033234b23 IDAC: Added better doc, fixed timetrial event 2023-10-01 21:41:08 +02:00
Dniel97
e0265485ff Initial D THE ARCADE S2 support added 2023-10-01 03:54:23 +02:00
Dniel97
38c1c31cf5 Merge branch 'develop' into fork_develop 2023-09-25 22:48:53 +02:00
Midorica
91791813dc Merge pull request 'ONGEKI Rival Functionality' (#36) from 2TT/artemis:develop into develop
Reviewed-on: https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/36
2023-09-23 16:08:15 +00:00
Kevin Trocolli
1497bf4a95 frontend: fixes to searching 2023-09-19 02:42:13 -04:00
Kevin Trocolli
aa17f99252 frontend: add placeholders, arcade search by IP 2023-09-19 02:31:29 -04:00
Kevin Trocolli
50de0916d4 allnet: minor tweeks to mirrior realnet better 2023-09-19 02:15:22 -04:00
Midorica
2affd0aae9 Merge pull request 'Fix wrong id being used to check for Chunithm WORLD'S END charts' (#33) from ASleepyCat/artemis:fix-worlds-end-id into develop
Reviewed-on: https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/33
2023-09-12 04:36:59 +00:00
ASleepyCat
4a916cb4d1 Fix reading WORLD'S END charts for SDBT 2023-09-10 12:29:19 +10:00
ASleepyCat
6c98f5f0a7 Fix wrong id being used to check for WORLD'S END charts 2023-09-10 12:29:19 +10:00
Midorica
090b3148d8 adding encryption support for ongeki 2023-09-09 12:03:42 -04:00
Wanich Keatkajonjumroen
ed46ea33e3 Added version select in session, removed get_profile_data_ignore_version, renamed scripts file 2023-09-09 06:55:18 +09:00
2TT
d584b93ca5 Merge branch 'develop' into develop 2023-09-02 06:39:37 +00:00
Wanich Keatkajonjumroen
d5d2803cc0 forgot a print command, see previous commit 2023-09-02 07:50:09 +09:00
Wanich Keatkajonjumroen
16d801aff5 Rival Delete functionality 2023-09-02 07:40:57 +09:00
Wanich Keatkajonjumroen
147d7adaaf Frontend for adding rivals, versionless backend function to support 2023-09-02 06:21:02 +09:00
Wanich Keatkajonjumroen
1f545aed41 removed print statements 2023-09-02 05:04:59 +09:00
Wanich Keatkajonjumroen
eaf9e0cf09 removed temp file 2023-08-30 13:53:55 +09:00
Wanich Keatkajonjumroen
0d7409906a fixed backend ongeki rival functionality 2023-08-30 13:53:45 +09:00
Wanich Keatkajonjumroen
5cccc9224a geki version display 2023-08-30 13:53:41 +09:00
Wanich Keatkajonjumroen
2a52e391d6 Ongeki template start 2023-08-30 13:53:31 +09:00
Kevin Trocolli
fc947d36a5 mai2: fix get user music for dx and up 2023-07-24 01:32:56 -04:00
Kevin Trocolli
5f586379ca Merge branch 'develop' 2023-07-16 17:25:18 -04:00
EmmyHeart
3c7ceabf4e And again 2023-07-11 10:04:25 +00:00
EmmyHeart
1bc8648e35 I knew I forgot something (fixed config) 2023-07-11 09:56:09 +00:00
EmmyHeart
eecd3a829d Added value for team rank scaling, and documented it a bit 2023-07-11 09:40:49 +00:00
EmmyHeart
b42e8ab76c Added function for pulling a song via the DB unique ID instead of the native song ID 2023-07-11 09:16:11 +00:00
EmmyHeart
043ff17008 Add team support, rivals, and test function for getting playcounts 2023-07-11 09:14:53 +00:00
EmmyHeart
c01d3f49f5 Added call for getting rival's music lists 2023-07-11 09:13:19 +00:00
EmmyHeart
75842b5a88 Add team support, implement team upsert, add rivals 2023-07-11 09:12:34 +00:00
Hay1tsme
fe4dfe369b Update 'readme.md' 2023-04-30 01:18:00 +00:00
Kevin Trocolli
02040300b8 Merge branch 'develop' 2023-04-23 21:05:25 -04:00
Kevin Trocolli
780a96ec15 Merge branch 'develop' 2023-03-19 23:33:01 -04:00
Kevin Trocolli
b8b93a8d51 Merge branch 'develop' 2023-03-18 23:56:23 -04:00
Hay1tsme
a0e24c6742 Merge branch 'develop' 2023-03-03 17:07:58 -05:00
Hay1tsme
90024ddbd9 Merge branch 'develop' 2023-03-03 13:19:55 -05:00
Hay1tsme
be00ad8e96 wacca: add lily to list of items given on profile create, fixes #4 2023-03-01 23:55:29 -05:00
Hay1tsme
e2129b45b7 Merge branch 'develop' 2023-03-01 23:41:42 -05:00
Hay1tsme
670747cf48 add platform_system to requirements.txt 2023-02-24 13:35:37 -05:00
Hay1tsme
83f09e180e add protobuf to requirements, fixes #2 2023-02-22 10:01:42 -05:00
Hay1tsme
a83b717821 carry over database functions from megaime 2023-02-21 14:05:55 -05:00
223 changed files with 36794 additions and 2037 deletions

3
.gitattributes vendored Normal file
View File

@@ -0,0 +1,3 @@
*.csv binary
*.txt binary
*.json binary

View File

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

View File

@@ -1,6 +1,101 @@
# 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.
## 20240109
### System
+ Removed `ADD config config` from dockerfile
### 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)

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ 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 *
@@ -145,8 +146,16 @@ class AimedbProtocol(Protocol):
def handle_lookup(self, data: bytes, resp_code: int) -> ADBBaseResponse: 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 = self.data.card.get_user_id_from_card(req.access_code)
is_banned = self.data.card.get_card_banned(req.access_code)
is_locked = 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}"
@@ -157,11 +166,29 @@ class AimedbProtocol(Protocol):
req = ADBLookupRequest(data) req = ADBLookupRequest(data)
user_id = self.data.card.get_user_id_from_card(req.access_code) user_id = self.data.card.get_user_id_from_card(req.access_code)
is_banned = self.data.card.get_card_banned(req.access_code)
is_locked = 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
return ret return ret
def handle_felica_lookup(self, data: bytes, resp_code: int) -> bytes: def handle_felica_lookup(self, data: bytes, resp_code: int) -> bytes:
@@ -224,7 +251,17 @@ 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)
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
return resp
def handle_campaign_clear(self, data: bytes, resp_code: int) -> ADBBaseResponse: def handle_campaign_clear(self, data: bytes, resp_code: int) -> ADBBaseResponse:
req = ADBCampaignClearRequest(data) req = ADBCampaignClearRequest(data)

View File

@@ -16,10 +16,11 @@ from os import path
import urllib.parse import urllib.parse
import math 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 +34,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:
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.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"):
@@ -70,18 +125,8 @@ class AllnetServlet:
if len(plugins) == 0: if len(plugins) == 0:
self.logger.error("No games detected!") 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( self.logger.info(
f"Serving {len(self.uri_registry)} game codes port {core_cfg.allnet.port}" f"Serving {len(TitleServlet.title_registry)} game codes port {core_cfg.allnet.port}"
) )
def handle_poweron(self, request: Request, _: Dict): def handle_poweron(self, request: Request, _: Dict):
@@ -147,7 +192,7 @@ class AllnetServlet:
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 (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8")
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( self.data.base.log_event(
"allnet", "ALLNET_AUTH_NO_SHOP_IP", logging.ERROR, msg "allnet", "ALLNET_AUTH_NO_SHOP_IP", logging.ERROR, msg
@@ -176,21 +221,21 @@ 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( self.data.base.log_event(
@@ -215,11 +260,9 @@ class AllnetServlet:
self.logger.debug(f"Allnet response: {resp_str}") self.logger.debug(f"Allnet response: {resp_str}")
return (resp_str + "\n").encode("utf-8") return (resp_str + "\n").encode("utf-8")
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) resp.uri, resp.host = TitleServlet.title_registry[req.game_id].get_allnet_info(req.game_id, int(int_ver), req.serial)
resp.host = resp.host.replace("$v", int_ver)
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) self.data.base.log_event("allnet", "ALLNET_AUTH_SUCCESS", logging.INFO, msg)
@@ -296,7 +339,6 @@ class AllnetServlet:
return res_str return res_str
def handle_dlorder_ini(self, request: Request, match: Dict) -> bytes: def handle_dlorder_ini(self, request: Request, match: Dict) -> bytes:
if "file" not in match: if "file" not in match:
return b"" return b""
@@ -316,6 +358,7 @@ class AllnetServlet:
def handle_dlorder_report(self, request: Request, match: Dict) -> bytes: def handle_dlorder_report(self, request: Request, match: Dict) -> bytes:
req_raw = request.content.getvalue() req_raw = request.content.getvalue()
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:
@@ -333,12 +376,18 @@ class AllnetServlet:
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 "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 "NG"
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."
self.data.base.log_event("allnet", "DL_REPORT", logging.INFO, msg, dl_data)
self.logger.info(msg)
return "OK" return "OK"
def handle_loaderstaterecorder(self, request: Request, match: Dict) -> bytes: def handle_loaderstaterecorder(self, request: Request, match: Dict) -> bytes:
@@ -383,16 +432,17 @@ class AllnetServlet:
signer = PKCS1_v1_5.new(rsa) signer = PKCS1_v1_5.new(rsa)
digest = SHA.new() digest = SHA.new()
traces: List[TraceData] = [] traces: List[TraceData] = []
try: try:
for x in range(len(req_dict)): req = BillingInfo(req_dict[0])
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()
for x in range(1, len(req_dict)):
if not req_dict[x]: if not req_dict[x]:
continue continue
if x == 0: try:
req = BillingInfo(req_dict[x])
continue
tmp = TraceData(req_dict[x]) tmp = TraceData(req_dict[x])
if tmp.trace_type == TraceDataType.CHARGE: if tmp.trace_type == TraceDataType.CHARGE:
tmp = TraceDataCharge(req_dict[x]) tmp = TraceDataCharge(req_dict[x])
@@ -403,11 +453,11 @@ class AllnetServlet:
traces.append(tmp) traces.append(tmp)
except KeyError as e:
self.logger.warn(f"Tracelog failed to parse: {e}")
kc_serial_bytes = req.keychipid.encode() 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) machine = self.data.arcade.get_machine(req.keychipid)
if machine is None and not self.config.server.allow_unregistered_serials: if machine is None and not self.config.server.allow_unregistered_serials:
@@ -525,7 +575,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"
@@ -644,9 +694,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))
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.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 +712,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))
@@ -740,13 +798,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 +812,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 +820,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,11 +840,11 @@ 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

View File

@@ -36,6 +36,12 @@ class ServerConfig:
self.__config, "core", "server", "is_develop", default=True self.__config, "core", "server", "is_develop", default=True
) )
@property
def is_using_proxy(self) -> bool:
return CoreConfig.get_config_field(
self.__config, "core", "server", "is_using_proxy", default=False
)
@property @property
def threading(self) -> bool: def threading(self) -> bool:
return CoreConfig.get_config_field( return CoreConfig.get_config_field(
@@ -85,6 +91,36 @@ class TitleConfig:
self.__config, "core", "title", "port", default=8080 self.__config, "core", "title", "port", default=8080
) )
@property
def port_ssl(self) -> int:
return CoreConfig.get_config_field(
self.__config, "core", "title", "port_ssl", default=0
)
@property
def ssl_key(self) -> str:
return CoreConfig.get_config_field(
self.__config, "core", "title", "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
def reboot_start_time(self) -> str:
return CoreConfig.get_config_field(
self.__config, "core", "title", "reboot_start_time", default=""
)
@property
def reboot_end_time(self) -> str:
return CoreConfig.get_config_field(
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:
@@ -150,6 +186,12 @@ class DatabaseConfig:
default=10000, default=10000,
) )
@property
def enable_memcached(self) -> bool:
return CoreConfig.get_config_field(
self.__config, "core", "database", "enable_memcached", default=True
)
@property @property
def memcached_host(self) -> str: def memcached_host(self) -> str:
return CoreConfig.get_config_field( return CoreConfig.get_config_field(
@@ -272,6 +314,18 @@ 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:

View File

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

View File

@@ -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),
@@ -218,9 +217,16 @@ class ArcadeData(BaseData):
return True return True
def find_arcade_by_name(self, name: str) -> List[Row]: 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 = self.execute(sql)
if result is None: if result is None:
return False return None
return result.fetchall()
def get_arcades_by_ip(self, ip: str) -> Optional[List[Row]]:
sql = arcade.select().where(arcade.c.ip == ip)
result = self.execute(sql)
if result is None:
return None
return result.fetchall() return result.fetchall()

View File

@@ -157,6 +157,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":

View File

@@ -64,6 +64,27 @@ class CardData(BaseData):
return int(card["user"]) return int(card["user"])
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 = self.get_card_by_access_code(access_code)
if card is None:
return None
if card["is_banned"]:
return True
return False
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 = self.get_card_by_access_code(access_code)
if card is None:
return None
if card["is_locked"]:
return True
return False
def delete_card(self, card_id: int) -> None: 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)

View File

@@ -0,0 +1,2 @@
ALTER TABLE diva_profile
DROP skn_eqp;

View File

@@ -0,0 +1,2 @@
ALTER TABLE diva_profile
ADD skn_eqp INT NOT NULL DEFAULT 0;

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

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

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

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

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

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

View File

@@ -21,6 +21,7 @@ class IUserSession(Interface):
userId = Attribute("User's ID") userId = Attribute("User's ID")
current_ip = Attribute("User's current ip address") current_ip = Attribute("User's current ip address")
permissions = Attribute("User's permission level") permissions = Attribute("User's permission level")
ongeki_version = Attribute("User's selected Ongeki Version")
class PermissionOffset(Enum): class PermissionOffset(Enum):
USER = 0 # Regular user USER = 0 # Regular user
@@ -36,6 +37,7 @@ class UserSession(object):
self.userId = 0 self.userId = 0
self.current_ip = "0.0.0.0" self.current_ip = "0.0.0.0"
self.permissions = 0 self.permissions = 0
self.ongeki_version = 7
class FrontendServlet(resource.Resource): class FrontendServlet(resource.Resource):
@@ -304,9 +306,9 @@ class FE_System(FE_Base):
def render_GET(self, request: Request): def render_GET(self, request: Request):
uri = request.uri.decode() uri = request.uri.decode()
template = self.environment.get_template("core/frontend/sys/index.jinja") template = self.environment.get_template("core/frontend/sys/index.jinja")
usrlist = [] usrlist: List[Dict] = []
aclist = [] aclist: List[Dict] = []
cablist = [] cablist: List[Dict] = []
sesh: Session = request.getSession() sesh: Session = request.getSession()
usr_sesh = IUserSession(sesh) usr_sesh = IUserSession(sesh)
@@ -339,6 +341,7 @@ class FE_System(FE_Base):
ac_id_search = uri_parse.get("arcadeId") ac_id_search = uri_parse.get("arcadeId")
ac_name_search = uri_parse.get("arcadeName") ac_name_search = uri_parse.get("arcadeName")
ac_user_search = uri_parse.get("arcadeUser") ac_user_search = uri_parse.get("arcadeUser")
ac_ip_search = uri_parse.get("arcadeIp")
if ac_id_search is not None: if ac_id_search is not None:
u = self.data.arcade.get_arcade(ac_id_search[0]) u = self.data.arcade.get_arcade(ac_id_search[0])
@@ -346,12 +349,20 @@ class FE_System(FE_Base):
aclist.append(u._asdict()) aclist.append(u._asdict())
elif ac_name_search is not None: elif ac_name_search is not None:
ul = self.data.arcade.find_arcade_by_name(ac_name_search[0]) ul = self.data.arcade.get_arcade_by_name(ac_name_search[0])
if ul is not None:
for u in ul: for u in ul:
aclist.append(u._asdict()) aclist.append(u._asdict())
elif ac_user_search is not None: elif ac_user_search is not None:
ul = self.data.arcade.get_arcades_managed_by_user(ac_user_search[0]) ul = self.data.arcade.get_arcades_managed_by_user(ac_user_search[0])
if ul is not None:
for u in ul:
aclist.append(u._asdict())
elif ac_ip_search is not None:
ul = self.data.arcade.get_arcades_by_ip(ac_ip_search[0])
if ul is not None:
for u in ul: for u in ul:
aclist.append(u._asdict()) aclist.append(u._asdict())

View File

@@ -4,6 +4,7 @@
<title>{{ title }}</title> <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"> <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/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> <style>
html { html {
background-color: #181a1b !important; background-color: #181a1b !important;
@@ -77,6 +78,9 @@
margin-bottom: 10px; margin-bottom: 10px;
width: 15%; width: 15%;
} }
.modal-content {
background-color: #181a1b;
}
</style> </style>
</head> </head>
<body> <body>

View File

@@ -8,8 +8,8 @@
<form id="usrLookup" name="usrLookup" action="/sys/lookup.user" class="form-inline"> <form id="usrLookup" name="usrLookup" action="/sys/lookup.user" class="form-inline">
<h3>User Search</h3> <h3>User Search</h3>
<div class="form-group"> <div class="form-group">
<label for="usrEmail">Email address</label> <label for="usrId">User ID</label>
<input type="email" class="form-control" id="usrEmail" name="usrEmail" aria-describedby="emailHelp"> <input type="number" class="form-control" id="usrId" name="usrId">
</div> </div>
OR OR
<div class="form-group"> <div class="form-group">
@@ -18,8 +18,8 @@
</div> </div>
OR OR
<div class="form-group"> <div class="form-group">
<label for="usrId">User ID</label> <label for="usrEmail">Email address</label>
<input type="number" class="form-control" id="usrId" name="usrId"> <input type="email" class="form-control" id="usrEmail" name="usrEmail" aria-describedby="emailHelp">
</div> </div>
<br /> <br />
<button type="submit" class="btn btn-primary">Search</button> <button type="submit" class="btn btn-primary">Search</button>
@@ -30,20 +30,25 @@
<div class="col-sm-6" style="max-width: 25%;"> <div class="col-sm-6" style="max-width: 25%;">
<form id="arcadeLookup" name="arcadeLookup" action="/sys/lookup.arcade" class="form-inline" > <form id="arcadeLookup" name="arcadeLookup" action="/sys/lookup.arcade" class="form-inline" >
<h3>Arcade Search</h3> <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"> <div class="form-group">
<label for="arcadeId">Arcade ID</label> <label for="arcadeId">Arcade ID</label>
<input type="number" class="form-control" id="arcadeId" name="arcadeId"> <input type="number" class="form-control" id="arcadeId" name="arcadeId">
</div> </div>
OR OR
<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"> <div class="form-group">
<label for="arcadeUser">Owner User ID</label> <label for="arcadeUser">Owner User ID</label>
<input type="number" class="form-control" id="arcadeUser" name="arcadeUser"> <input type="number" class="form-control" id="arcadeUser" name="arcadeUser">
</div> </div>
OR
<div class="form-group">
<label for="arcadeIp">Assigned IP Address</label>
<input type="text" class="form-control" id="arcadeIp" name="arcadeIp">
</div>
<br /> <br />
<button type="submit" class="btn btn-primary">Search</button> <button type="submit" class="btn btn-primary">Search</button>
</form> </form>
@@ -52,13 +57,13 @@
<form id="cabLookup" name="cabLookup" action="/sys/lookup.cab" class="form-inline" > <form id="cabLookup" name="cabLookup" action="/sys/lookup.cab" class="form-inline" >
<h3>Machine Search</h3> <h3>Machine Search</h3>
<div class="form-group"> <div class="form-group">
<label for="cabSerial">Machine Serial</label> <label for="cabId">Machine ID</label>
<input type="text" class="form-control" id="cabSerial" name="cabSerial"> <input type="number" class="form-control" id="cabId" name="cabId">
</div> </div>
OR OR
<div class="form-group"> <div class="form-group">
<label for="cabId">Machine ID</label> <label for="cabSerial">Machine Serial</label>
<input type="number" class="form-control" id="cabId" name="cabId"> <input type="text" class="form-control" id="cabSerial" name="cabSerial">
</div> </div>
OR OR
<div class="form-group"> <div class="form-group">
@@ -75,19 +80,19 @@
{% if sesh.permissions >= 2 %} {% if sesh.permissions >= 2 %}
<div id="userSearchResult" class="col-sm-6" style="max-width: 25%;"> <div id="userSearchResult" class="col-sm-6" style="max-width: 25%;">
{% for usr in usrlist %} {% for usr in usrlist %}
<pre><a href=/user/{{ usr.id }}>{{ usr.id }} | {{ usr.username }}</a></pre> <a href=/user/{{ usr.id }}><pre>{{ usr.id }} | {{ usr.username if usr.username != None else "<i>No Name Set</i>"}}</pre></a>
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}
{% if sesh.permissions >= 4 %} {% if sesh.permissions >= 4 %}
<div id="arcadeSearchResult" class="col-sm-6" style="max-width: 25%;"> <div id="arcadeSearchResult" class="col-sm-6" style="max-width: 25%;">
{% for ac in aclist %} {% for ac in aclist %}
<pre><a href=/arcade/{{ ac.id }}>{{ ac.id }} | {{ ac.name }}</a></pre> <pre><a href=/arcade/{{ ac.id }}>{{ ac.id }} | {{ ac.name if ac.name != None else "<i>No Name Set</i>" }} | {{ ac.ip if ac.ip != None else "<i>No IP Assigned</i>"}}</pre></a>
{% endfor %} {% endfor %}
</div </div
><div id="cabSearchResult" class="col-sm-6" style="max-width: 25%;"> ><div id="cabSearchResult" class="col-sm-6" style="max-width: 25%;">
{% for cab in cablist %} {% 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> <a href=/cab/{{ cab.id }}><pre>{{ cab.id }} | {{ cab.game if cab.game != None else "<i>ANY </i>" }} | {{ cab.serial }}</pre></a>
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}

View File

@@ -7,15 +7,15 @@ 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
class MuchaServlet: class MuchaServlet:
mucha_registry: List[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,11 +37,9 @@ 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() for _, mod in TitleServlet.title_registry.items():
if hasattr(mod, "get_mucha_info"):
for _, mod in all_titles.items(): enabled, game_cd = mod.get_mucha_info(
if hasattr(mod, "index") and hasattr(mod.index, "get_mucha_info"):
enabled, game_cd = mod.index.get_mucha_info(
self.config, self.config_dir self.config, self.config_dir
) )
if enabled: if enabled:

View File

@@ -1,4 +1,4 @@
from typing import Dict, Any from typing import Dict, List, Tuple
import logging, coloredlogs import logging, coloredlogs
from logging.handlers import TimedRotatingFileHandler from logging.handlers import TimedRotatingFileHandler
from twisted.web.http import Request from twisted.web.http import Request
@@ -7,14 +7,88 @@ 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 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_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]:
"""Called during boot to get all matcher endpoints this title servlet handles
Returns:
Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: A 2-length tuple where offset 0 is GET and offset 1 is POST,
containing a list of 3-length tuples where offset 0 is the name of the function in the handler that should be called, offset 1
is the matching string, and offset 2 is a dict containing rules for the matcher.
"""
return (
[("render_GET", "/{game}/{version}/{endpoint}", {'game': R'S...'})],
[("render_POST", "/{game}/{version}/{endpoint}", {'game': R'S...'})]
)
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.title.hostname}:{Utils.get_title_port(self.core_cfg)}/{game_code}/{game_ver}/", "")
return (f"http://{self.core_cfg.title.hostname}/{game_code}/{game_ver}/", "")
def get_mucha_info(self, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, 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, "")
def render_POST(self, request: Request, game_code: str, matchers: Dict) -> bytes:
self.logger.warn(f"{game_code} Does not dispatch POST")
return None
def render_GET(self, request: Request, game_code: str, matchers: Dict) -> bytes:
self.logger.warn(f"{game_code} Does not dispatch GET")
return None
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,17 +117,14 @@ 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:
handler_cls = mod.index(self.config, self.config_folder)
if hasattr(handler_cls, "setup") and should_call_setup: if hasattr(handler_cls, "setup") and should_call_setup:
handler_cls.setup() handler_cls.setup()
@@ -62,43 +133,46 @@ class TitleServlet:
self.title_registry[code] = handler_cls self.title_registry[code] = handler_cls
else: else:
self.logger.warning(f"Game {folder} has no get_allnet_info") self.logger.error(f"{folder} missing game_code or index in __init__.py, or is_game_enabled in index")
else:
self.logger.error(f"{folder} missing game_code or index in __init__.py")
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.title.port) if core_cfg.title.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)

View File

@@ -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 twisted.web.http 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] = {}
@@ -33,3 +39,59 @@ class Utils:
if b"x-forwarded-for" in req.getAllHeaders() if b"x-forwarded-for" in req.getAllHeaders()
else req.getClientAddress().host 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
if cfg.title.port == 0:
cls.real_title_port = cfg.allnet.port
else:
cls.real_title_port = cfg.title.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
if cfg.title.port_ssl == 0:
cls.real_title_port_ssl = 443
else:
cls.real_title_port_ssl = cfg.title.port_ssl
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

View File

@@ -5,13 +5,14 @@ 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:
@@ -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
View 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

View File

@@ -10,11 +10,11 @@ This step-by-step guide assumes that you are using a fresh install of Windows 10
3. Make sure that you enable "Create shortcuts for installed applications" and "Add Python to environment variables" and hit Install 3. Make sure that you enable "Create shortcuts for installed applications" and "Add Python to environment variables" and hit Install
## Install MySQL 8.0 ## Install MySQL 8.0
1. Download MySQL 8.0 Server : [Link](https://cdn.mysql.com//Downloads/MySQLInstaller/mysql-installer-web-community-8.0.31.0.msi) 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.31.0.msi 2. Install mysql-installer-web-community-8.0.34.0.msi
1. Click on "Add ..." on the side 1. Click on "Add ..." on the side
2. Click on the "+" next to MySQL Servers 2. Click on the "+" next to MySQL Servers
3. Make sure MySQL Server 8.0.29 - X64 is under the products to be installed. 3. Make sure MySQL Server 8.0.34 - X64 is under the products to be installed.
4. Hit Next and Next once installed 4. Hit Next and Next once installed
5. Select the configuration type "Development Computer" 5. Select the configuration type "Development Computer"
6. Hit Next 6. Hit Next
@@ -23,9 +23,10 @@ This step-by-step guide assumes that you are using a fresh install of Windows 10
9. Leave everything under Windows Service as default and 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 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 3. Open MySQL 8.0 Command Line Client and login as your root user
4. Type those commands to create your user and the database 4. Change `<Enter Password Here>` to a new password for the user aime, type those commands to create your user and the database
```
CREATE USER 'aime'@'localhost' IDENTIFIED BY 'MyStrongPass.'; ```sql
CREATE USER 'aime'@'localhost' IDENTIFIED BY '<Enter Password Here>';
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; FLUSH PRIVILEGES;
@@ -34,33 +35,50 @@ exit;
## Install Python modules ## Install Python modules
1. Change your work path to the artemis-master folder using 'cd' and install the requirements: 1. Change your work path to the artemis-master folder using 'cd' and install the requirements:
> pip install -r requirements.txt
## Copy/Rename the folder example_config to config ```shell
pip install -r requirements.txt
## Adjust /config/core.yaml
1. Make sure to change the server listen_address to be set to your local machine IP (ex.: 192.168.1.xxx)
- In case you want to run this only locally, set the following values:
``` ```
## 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: server:
listen_address: 0.0.0.0 listen_address: 0.0.0.0
title: title:
hostname: localhost 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"
``` ```
2. Adjust the proper MySQL information you created earlier
3. Add the AimeDB key at the bottom of the file 3. Add the AimeDB key at the bottom of the file
4. If the webui is needed, change the flag from False to True 4. If the webui is needed, change the flag from False to True
## Create the database tables for ARTEMiS ## Create the database tables for ARTEMiS
> python dbutils.py create
```shell
python dbutils.py create
```
## Firewall Adjustements ## 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): 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 > Port 80 (TCP), 443 (TCP), 8443 (TCP), 22345 (TCP), 8080 (TCP), 8090 (TCP) **webui, 8444 (TCP) **mucha
## Running the ARTEMiS instance ## Running the ARTEMiS instance
> python index.py ```shell
python index.py
```
# Troubleshooting # Troubleshooting
@@ -78,6 +96,7 @@ Make sure the following ports are open both on your router and local Windows fir
## AttributeError: module 'collections' has no attribute 'Hashable' ## 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. 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: - 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 pip install -r requirements.txt -U
``` ```

View File

@@ -6,11 +6,18 @@
- `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 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`
- `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` - `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`
- `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
- `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`
## 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` - `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 title server will listen for connections on. Set to 0 to use the Allnet handler to reduce the port footprint. Default `8080` - `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`
- `port_ssl`: Port that the secure title server will listen for connections on. Set to 0 to use the Allnet handler to reduce the port footprint. Default `0`
- `ssl_key`: Location of the ssl server key for the secure title server. Ignored if `port_ssl` is set to `0` or `is_develop` set to `False`. 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 `port_ssl` is set to `0` or `is_develop` is set to `False`. Default `cert/title.pem`
- `reboot_start_time`: 24 hour JST time that clients will see as the start of maintenance period. Leave blank for no maintenance time. Default: ""
- `reboot_end_time`: 24 hour JST time that clients will see as the end of maintenance period. 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`

View File

@@ -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
@@ -59,11 +67,30 @@ python read.py --game SDBT --version <version ID> --binfolder /path/to/game/fold
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 +115,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 |
@@ -114,26 +165,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,18 +179,19 @@ 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 |
### Importer ### Importer
@@ -174,11 +212,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 +225,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 |
@@ -207,16 +246,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 +267,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 |
@@ -255,42 +295,118 @@ 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)
* O.N.G.E.K.I. bright MEMORY: Yes
### Importer ### Importer
@@ -391,7 +507,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 |
@@ -414,7 +530,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 +539,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
@@ -469,7 +585,7 @@ 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 |
@@ -488,7 +604,7 @@ 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 |
@@ -496,7 +612,7 @@ Config file is located in `config/sao.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. `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
@@ -509,8 +625,141 @@ python dbutils.py --game SDEW upgrade
- 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
**Network hashing in GssSite.dll must be disabled**
### Credits for SAO support: ### Credits for SAO support:
- Midorica - Limited Network Support - Midorica - Limited 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 | Akina Lake(秋名湖) | CounterClockwise(左周り) |
| 2 | Akina Lake(秋名湖) | 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(逆走) |
| 56 | Momiji Line(もみじライン) | Downhill(下り) |
| 58 | Momiji Line(もみじライン) | Hillclimb(上り) |
| 20 | Tsukuba(筑波) | Outbound(往路) |
| 22 | Tsukuba(筑波) | Inbound(復路) |
| 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(逆走) |
### 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.

View File

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

View File

@@ -4,6 +4,7 @@ server:
allow_unregistered_serials: True allow_unregistered_serials: True
name: "ARTEMiS" name: "ARTEMiS"
is_develop: True is_develop: True
is_using_proxy: False
threading: False threading: False
log_dir: "logs" log_dir: "logs"
check_arcade_ip: False check_arcade_ip: False
@@ -13,6 +14,12 @@ title:
loglevel: "info" loglevel: "info"
hostname: "localhost" hostname: "localhost"
port: 8080 port: 8080
port_ssl: 0
ssl_cert: "cert/title.crt"
ssl_key: "cert/title.key"
reboot_start_time: "04:00"
reboot_end_time: "05:00"
database: database:
host: "localhost" host: "localhost"
@@ -24,6 +31,7 @@ database:
sha2_password: False sha2_password: False
loglevel: "warn" loglevel: "warn"
user_table_autoincrement_start: 10000 user_table_autoincrement_start: 10000
enable_memcached: True
memcached_host: "localhost" memcached_host: "localhost"
frontend: frontend:
@@ -48,6 +56,8 @@ aimedb:
loglevel: "info" loglevel: "info"
port: 22345 port: 22345
key: "" key: ""
id_secret: ""
id_lifetime_seconds: 86400
mucha: mucha:
enable: False enable: False

View File

@@ -1,9 +1,3 @@
server: server:
enable: True enable: True
loglevel: "info" loglevel: "info"
hostname: "localhost"
ssl_enable: False
port: 8082
port_secure: 443
ssl_cert: "cert/title.crt"
ssl_key: "cert/title.key"

22
example_config/idac.yaml Normal file
View File

@@ -0,0 +1,22 @@
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
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"

View File

@@ -24,8 +24,7 @@ server {
# 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;
@@ -55,7 +54,7 @@ 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; ssl_prefer_server_ciphers off;
location / { location / {
@@ -75,7 +74,7 @@ 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; ssl_prefer_server_ciphers off;
location / { location / {
@@ -85,28 +84,6 @@ server {
} }
} }
# 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/;
}
}
# Frontend, set to redirect to HTTPS. Comment out if you don't intend to use the frontend # Frontend, set to redirect to HTTPS. Comment out if you don't intend to use the frontend
server { server {
listen 80; listen 80;

View File

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

View File

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

View File

@@ -22,8 +22,8 @@ class HttpDispatcher(resource.Resource):
self.map_post = Mapper() self.map_post = Mapper()
self.logger = logging.getLogger("core") self.logger = logging.getLogger("core")
self.allnet = AllnetServlet(cfg, config_dir)
self.title = TitleServlet(cfg, config_dir) self.title = TitleServlet(cfg, config_dir)
self.allnet = AllnetServlet(cfg, config_dir)
self.mucha = MuchaServlet(cfg, config_dir) self.mucha = MuchaServlet(cfg, config_dir)
self.map_get.connect( self.map_get.connect(
@@ -144,21 +144,31 @@ class HttpDispatcher(resource.Resource):
conditions=dict(method=["POST"]), conditions=dict(method=["POST"]),
) )
for code, game in self.title.title_registry.items():
get_matchers, post_matchers = game.get_endpoint_matchers()
for m in get_matchers:
self.map_get.connect( self.map_get.connect(
"title_get", "title_get",
"/{game}/{version}/{endpoint:.*?}", m[1],
controller="title", controller="title",
action="render_GET", action="render_GET",
title=code,
subaction=m[0],
conditions=dict(method=["GET"]), conditions=dict(method=["GET"]),
requirements=dict(game=R"S..."), requirements=m[2],
) )
for m in post_matchers:
self.map_post.connect( self.map_post.connect(
"title_post", "title_post",
"/{game}/{version}/{endpoint:.*?}", m[1],
controller="title", controller="title",
action="render_POST", action="render_POST",
title=code,
subaction=m[0],
conditions=dict(method=["POST"]), conditions=dict(method=["POST"]),
requirements=dict(game=R"S..."), requirements=m[2],
) )
def render_GET(self, request: Request) -> bytes: def render_GET(self, request: Request) -> bytes:
@@ -279,6 +289,7 @@ if __name__ == "__main__":
allnet_server_str = f"tcp:{cfg.allnet.port}:interface={cfg.server.listen_address}" 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}" title_server_str = f"tcp:{cfg.title.port}:interface={cfg.server.listen_address}"
title_https_server_str = f"ssl:{cfg.title.port_ssl}:interface={cfg.server.listen_address}:privateKey={cfg.title.ssl_key}:certKey={cfg.title.ssl_cert}"
adb_server_str = f"tcp:{cfg.aimedb.port}:interface={cfg.server.listen_address}" adb_server_str = f"tcp:{cfg.aimedb.port}:interface={cfg.server.listen_address}"
frontend_server_str = ( frontend_server_str = (
f"tcp:{cfg.frontend.port}:interface={cfg.server.listen_address}" f"tcp:{cfg.frontend.port}:interface={cfg.server.listen_address}"
@@ -313,6 +324,11 @@ if __name__ == "__main__":
server.Site(dispatcher) server.Site(dispatcher)
) )
if cfg.title.port_ssl > 0:
endpoints.serverFromString(reactor, title_https_server_str).listen(
server.Site(dispatcher)
)
if cfg.server.threading: if cfg.server.threading:
Thread(target=reactor.run, args=(False,)).start() Thread(target=reactor.run, args=(False,)).start()
else: else:

View File

@@ -5,13 +5,13 @@ A network service emulator for games running SEGA'S ALL.NET service, and similar
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 + CHUNITHM
+ All versions up to SUN + All versions up to SUN PLUS
+ crossbeats REV. + crossbeats REV.
+ All versions + omnimix + All versions + omnimix
+ maimai DX + maimai DX
+ All versions up to FESTiVAL + All versions up to FESTiVAL PLUS
+ Hatsune Miku: Project DIVA Arcade + Hatsune Miku: Project DIVA Arcade
+ All versions + All versions
@@ -33,6 +33,9 @@ Games listed below have been tested and confirmed working. Only game versions ol
+ Sword Art Online Arcade (partial support) + Sword Art Online Arcade (partial support)
+ Final + Final
+ Initial D THE ARCADE
+ Season 2
## 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)
- pip - pip
@@ -40,7 +43,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), [ubuntu](docs/INSTALL_UBUNTU.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.

View File

@@ -18,3 +18,4 @@ jinja2
protobuf protobuf
autobahn autobahn
pillow pillow
pyjwt

View File

@@ -6,5 +6,5 @@ from titles.chuni.read import ChuniReader
index = ChuniServlet index = ChuniServlet
database = ChuniData database = ChuniData
reader = ChuniReader reader = ChuniReader
game_codes = [ChuniConstants.GAME_CODE, ChuniConstants.GAME_CODE_NEW] game_codes = [ChuniConstants.GAME_CODE, ChuniConstants.GAME_CODE_NEW, ChuniConstants.GAME_CODE_INT]
current_schema_version = 4 current_schema_version = 5

View File

@@ -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:
@@ -181,21 +181,50 @@ class ChuniBase:
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: 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: def handle_get_game_ranking_api_request(self, data: Dict) -> Dict:
return {"type": data["type"], "gameRankingList": []} rankings = self.data.score.get_rankings(self.version)
return {"type": data["type"], "gameRankingList": rankings}
def handle_get_game_sale_api_request(self, data: Dict) -> Dict: 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: def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
# if reboot start/end time is not defined use the default behavior of being a few hours ago
if self.core_cfg.title.reboot_start_time == "" or self.core_cfg.title.reboot_end_time == "":
reboot_start = datetime.strftime( reboot_start = datetime.strftime(
datetime.now() - timedelta(hours=4), self.date_time_format datetime.utcnow() + timedelta(hours=6), self.date_time_format
) )
reboot_end = datetime.strftime( reboot_end = datetime.strftime(
datetime.now() - timedelta(hours=3), self.date_time_format 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,7 +240,6 @@ class ChuniBase:
"isDumpUpload": "false", "isDumpUpload": "false",
"isAou": "false", "isAou": "false",
} }
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 = self.data.profile.get_profile_activity( user_activity_list = self.data.profile.get_profile_activity(
data["userId"], data["kind"] data["userId"], data["kind"]
@@ -284,6 +312,13 @@ class ChuniBase:
"userChargeList": charge_list, "userChargeList": charge_list,
} }
def handle_get_user_recent_player_api_request(self, data: Dict) -> Dict:
return {
"userId": data["userId"],
"length": 0,
"userRecentPlayerList": [], # playUserId, playUserName, playDate, friendPoint
}
def handle_get_user_course_api_request(self, data: Dict) -> Dict: def handle_get_user_course_api_request(self, data: Dict) -> Dict:
user_course_list = self.data.score.get_courses(data["userId"]) user_course_list = self.data.score.get_courses(data["userId"])
if user_course_list is None: if user_course_list is None:
@@ -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()
@@ -361,11 +396,77 @@ class ChuniBase:
"userDuelList": duel_list, "userDuelList": duel_list,
} }
def handle_get_user_rival_data_api_request(self, data: Dict) -> Dict:
p = 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
}
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 = 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
def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict: 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 = self.data.item.get_all_favorites(
data["userId"], self.version, fav_kind=int(data["kind"]) data["userId"], self.version, fav_kind=int(data["kind"])
) )
@@ -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),
@@ -600,19 +716,34 @@ class ChuniBase:
} }
def handle_get_user_team_api_request(self, data: Dict) -> Dict: 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 = self.data.profile.get_profile_data(data["userId"], self.version)
if profile and profile["teamId"]:
# Get team by id
team = self.data.profile.get_team_by_id(profile["teamId"])
if team:
team_id = team["id"]
team_name = team["teamName"]
team_rank = 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"],
@@ -623,16 +754,57 @@ class ChuniBase:
return { return {
"userId": data["userId"], "userId": data["userId"],
"length": 0, "length": 0,
"nextIndex": 0, "nextIndex": -1,
"teamCourseSettingList": [], "teamCourseSettingList": [],
} }
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
}
],
}
def handle_get_team_course_rule_api_request(self, data: Dict) -> Dict: 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_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
}
],
} }
def handle_upsert_user_all_api_request(self, data: Dict) -> Dict: def handle_upsert_user_all_api_request(self, data: Dict) -> Dict:
@@ -703,15 +875,34 @@ class ChuniBase:
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
if playlog["playedUserName1"] is not None:
playlog["playedUserName1"] = self.read_wtf8(playlog["playedUserName1"]) playlog["playedUserName1"] = self.read_wtf8(playlog["playedUserName1"])
if playlog["playedUserName2"] is not None:
playlog["playedUserName2"] = self.read_wtf8(playlog["playedUserName2"]) playlog["playedUserName2"] = self.read_wtf8(playlog["playedUserName2"])
if playlog["playedUserName3"] is not None:
playlog["playedUserName3"] = self.read_wtf8(playlog["playedUserName3"]) playlog["playedUserName3"] = self.read_wtf8(playlog["playedUserName3"])
self.data.score.put_playlog(user_id, playlog) 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 = 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
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) self.data.item.put_map_area(user_id, map_area)
@@ -730,6 +921,10 @@ class ChuniBase:
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
return {"returnCode": "1"} return {"returnCode": "1"}
def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict:

View File

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

View File

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

View File

@@ -11,30 +11,32 @@ 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: class ChuniServlet(BaseServlet):
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 +59,7 @@ class ChuniServlet:
ChuniNew, ChuniNew,
ChuniNewPlus, ChuniNewPlus,
ChuniSun, ChuniSun,
ChuniSunPlus,
] ]
self.logger = logging.getLogger("chuni") self.logger = logging.getLogger("chuni")
@@ -98,8 +101,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]),
@@ -115,10 +124,19 @@ class ChuniServlet:
f"Hashed v{version} method {method_fixed} with {bytes.fromhex(keys[2])} to get {hash.hex()}" f"Hashed v{version} method {method_fixed} with {bytes.fromhex(keys[2])} to get {hash.hex()}"
) )
def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]:
return (
[],
[
("render_POST", "/{game}/{version}/ChuniServlet/{endpoint}", {}),
("render_POST", "/{game}/{version}/ChuniServlet/MatchingServer/{endpoint}", {})
]
)
@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,28 +144,31 @@ 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.title.hostname}:{Utils.get_title_port(self.core_cfg)}/{game_code}/{game_ver}/", self.core_cfg.title.hostname)
def render_POST(self, request: Request, version: int, url_path: str) -> bytes: return (f"http://{self.core_cfg.title.hostname}/{game_code}/{game_ver}/", self.core_cfg.title.hostname)
if url_path.lower() == "ping":
def render_POST(self, request: Request, game_code: str, matchers: Dict) -> bytes:
endpoint = matchers['endpoint']
version = int(matchers['version'])
game_code = matchers['game']
if endpoint.lower() == "ping":
return zlib.compress(b'{"returnCode": "1"}') return zlib.compress(b'{"returnCode": "1"}')
req_raw = request.content.getvalue() 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 game_code == "SDHD" or game_code == "SDBT": # JP
if version < 105: # 1.0 if version < 105: # 1.0
internal_ver = ChuniConstants.VER_CHUNITHM internal_ver = ChuniConstants.VER_CHUNITHM
elif version >= 105 and version < 110: # PLUS elif version >= 105 and version < 110: # PLUS
@@ -174,8 +195,21 @@ class ChuniServlet:
internal_ver = ChuniConstants.VER_CHUNITHM_NEW internal_ver = ChuniConstants.VER_CHUNITHM_NEW
elif version >= 205 and version < 210: # NEW PLUS!! elif version >= 205 and version < 210: # NEW PLUS!!
internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS
elif version >= 210: # SUN elif version >= 210 and version < 215: # SUN
internal_ver = ChuniConstants.VER_CHUNITHM_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.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
@@ -240,6 +274,7 @@ class ChuniServlet:
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)

View File

@@ -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,6 +23,16 @@ 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 _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"
def handle_get_game_setting_api_request(self, data: Dict) -> Dict: 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
@@ -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
) )
# if reboot start/end time is not defined use the default behavior of being a few hours ago
if self.core_cfg.title.reboot_start_time == "" or self.core_cfg.title.reboot_end_time == "":
reboot_start = datetime.strftime( reboot_start = datetime.strftime(
datetime.utcnow() + timedelta(hours=6), self.date_time_format datetime.utcnow() + timedelta(hours=6), self.date_time_format
) )
reboot_end = datetime.strftime( reboot_end = datetime.strftime(
datetime.utcnow() + timedelta(hours=7), self.date_time_format 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,15 +78,15 @@ 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.title.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.title.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.title.hostname}:{self.core_cfg.title.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.title.hostname}:{self.core_cfg.title.port}/SDHD/{self._interal_ver_to_intver()}/ChuniServlet/",
}, },
"isDumpUpload": False, "isDumpUpload": False,
"isAou": False, "isAou": False,
@@ -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
}

View File

@@ -1,38 +1,16 @@
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:
ret = super().handle_get_game_setting_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: def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
user_data = super().handle_cm_get_user_preview_api_request(data) user_data = super().handle_cm_get_user_preview_api_request(data)

View File

@@ -169,8 +169,10 @@ class ChuniReader(BaseReader):
fumen_path = MusicFumenData.find("file").find("path") fumen_path = MusicFumenData.find("file").find("path")
if fumen_path is not None: if fumen_path is not None:
chart_id = MusicFumenData.find("type").find("id").text chart_type = MusicFumenData.find("type")
if chart_id == "4": chart_id = chart_type.find("id").text
chart_diff = chart_type.find("str").text
if chart_diff == "WorldsEnd" and (chart_id == "4" or chart_id == "5"): # 4 in SDBT, 5 in SDHD
level = float(xml_root.find("starDifType").text) level = float(xml_root.find("starDifType").text)
we_chara = ( we_chara = (
xml_root.find("worldsEndTagName") xml_root.find("worldsEndTagName")

View File

@@ -637,3 +637,69 @@ class ChuniProfileData(BaseData):
if result is None: if result is None:
return None return None
return result.fetchall() return result.fetchall()
def get_team_by_id(self, team_id: int) -> Optional[Row]:
sql = select(team).where(team.c.id == team_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_team_rank(self, team_id: int) -> int:
# Normal ranking system, likely the one used in the real servers
# Query all teams sorted by 'teamPoint'
result = self.execute(
select(team.c.id).order_by(team.c.teamPoint.desc())
)
# Get the rank of the team with the given team_id
rank = None
for i, row in enumerate(result, start=1):
if row.id == team_id:
rank = i
break
# Return the rank if found, or a default rank otherwise
return rank if rank is not None else 0
# RIP scaled team ranking. Gone, but forgotten
# def get_team_rank_scaled(self, team_id: int) -> int:
def update_team(self, team_id: int, team_data: Dict) -> bool:
team_data["id"] = team_id
sql = insert(team).values(**team_data)
conflict = sql.on_duplicate_key_update(**team_data)
result = self.execute(conflict)
if result is None:
self.logger.warn(
f"update_team: Failed to update team! team id: {team_id}"
)
return False
return True
def get_rival(self, rival_id: int) -> Optional[Row]:
sql = select(profile).where(profile.c.user == rival_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_overview(self) -> Dict:
# Fetch and add up all the playcounts
playcount_sql = self.execute(select(profile.c.playCount))
if playcount_sql is None:
self.logger.warn(
f"get_overview: Couldn't pull playcounts"
)
return 0
total_play_count = 0;
for row in playcount_sql:
total_play_count += row[0]
return {
"total_play_count": total_play_count
}

View File

@@ -6,7 +6,7 @@ from sqlalchemy.schema import ForeignKey
from sqlalchemy.engine import Row from sqlalchemy.engine import Row
from sqlalchemy.sql import func, select from sqlalchemy.sql import func, select
from sqlalchemy.dialects.mysql import insert from sqlalchemy.dialects.mysql import insert
from sqlalchemy.sql.expression import exists
from core.data.schema import BaseData, metadata from core.data.schema import BaseData, metadata
course = Table( course = Table(
@@ -24,7 +24,7 @@ course = Table(
Column("scoreMax", Integer), Column("scoreMax", Integer),
Column("isFullCombo", Boolean), Column("isFullCombo", Boolean),
Column("isAllJustice", Boolean), Column("isAllJustice", Boolean),
Column("isSuccess", Boolean), Column("isSuccess", Integer),
Column("scoreRank", Integer), Column("scoreRank", Integer),
Column("eventId", Integer), Column("eventId", Integer),
Column("lastPlayDate", String(25)), Column("lastPlayDate", String(25)),
@@ -32,7 +32,7 @@ course = Table(
Column("param2", Integer), Column("param2", Integer),
Column("param3", Integer), Column("param3", Integer),
Column("param4", Integer), Column("param4", Integer),
Column("isClear", Boolean), Column("isClear", Integer),
Column("theoryCount", Integer), Column("theoryCount", Integer),
Column("orderId", Integer), Column("orderId", Integer),
Column("playerRating", Integer), Column("playerRating", Integer),
@@ -60,7 +60,7 @@ best_score = Table(
Column("maxComboCount", Integer), Column("maxComboCount", Integer),
Column("isFullCombo", Boolean), Column("isFullCombo", Boolean),
Column("isAllJustice", Boolean), Column("isAllJustice", Boolean),
Column("isSuccess", Boolean), Column("isSuccess", Integer),
Column("fullChain", Integer), Column("fullChain", Integer),
Column("maxChain", Integer), Column("maxChain", Integer),
Column("scoreRank", Integer), Column("scoreRank", Integer),
@@ -125,7 +125,7 @@ playlog = Table(
Column("characterId", Integer), Column("characterId", Integer),
Column("skillId", Integer), Column("skillId", Integer),
Column("playKind", Integer), Column("playKind", Integer),
Column("isClear", Boolean), Column("isClear", Integer),
Column("skillLevel", Integer), Column("skillLevel", Integer),
Column("skillEffect", Integer), Column("skillEffect", Integer),
Column("placeName", String(255)), Column("placeName", String(255)),
@@ -136,6 +136,7 @@ playlog = Table(
Column("judgeHeaven", Integer), Column("judgeHeaven", Integer),
Column("regionId", Integer), Column("regionId", Integer),
Column("machineType", Integer), Column("machineType", Integer),
Column("ticketId", Integer),
mysql_charset="utf8mb4" mysql_charset="utf8mb4"
) )
@@ -189,9 +190,28 @@ class ChuniScoreData(BaseData):
return None return None
return result.fetchall() return result.fetchall()
def put_playlog(self, aime_id: int, playlog_data: Dict) -> Optional[int]: def put_playlog(self, aime_id: int, playlog_data: Dict, version: int) -> Optional[int]:
# Calculate the ROM version that should be inserted into the DB, based on the version of the ggame being inserted
# We only need from Version 10 (Plost) and back, as newer versions include romVersion in their upsert
# This matters both for gameRankings, as well as a future DB update to keep version data separate
romVer = {
10: "1.50.0",
9: "1.45.0",
8: "1.40.0",
7: "1.35.0",
6: "1.30.0",
5: "1.25.0",
4: "1.20.0",
3: "1.15.0",
2: "1.10.0",
1: "1.05.0",
0: "1.00.0"
}
playlog_data["user"] = aime_id playlog_data["user"] = aime_id
playlog_data = self.fix_bools(playlog_data) playlog_data = self.fix_bools(playlog_data)
if "romVersion" not in playlog_data:
playlog_data["romVersion"] = romVer.get(version, "1.00.0")
sql = insert(playlog).values(**playlog_data) sql = insert(playlog).values(**playlog_data)
conflict = sql.on_duplicate_key_update(**playlog_data) conflict = sql.on_duplicate_key_update(**playlog_data)
@@ -200,3 +220,39 @@ class ChuniScoreData(BaseData):
if result is None: if result is None:
return None return None
return result.lastrowid return result.lastrowid
def get_rankings(self, version: int) -> Optional[List[Dict]]:
# Calculates the ROM version that should be fetched for rankings, based on the game version being retrieved
# This prevents tracks that are not accessible in your version from counting towards the 10 results
romVer = {
13: "2.10%",
12: "2.05%",
11: "2.00%",
10: "1.50%",
9: "1.45%",
8: "1.40%",
7: "1.35%",
6: "1.30%",
5: "1.25%",
4: "1.20%",
3: "1.15%",
2: "1.10%",
1: "1.05%",
0: "1.00%"
}
sql = select([playlog.c.musicId.label('id'), func.count(playlog.c.musicId).label('point')]).where((playlog.c.level != 4) & (playlog.c.romVersion.like(romVer.get(version, "%")))).group_by(playlog.c.musicId).order_by(func.count(playlog.c.musicId).desc()).limit(10)
result = self.execute(sql)
if result is None:
return None
rows = result.fetchall()
return [dict(row) for row in rows]
def get_rival_music(self, rival_id: int) -> Optional[List[Dict]]:
sql = select(best_score).where(best_score.c.user == rival_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()

View File

@@ -453,6 +453,15 @@ class ChuniStaticData(BaseData):
return None return None
return result.fetchone() return result.fetchone()
def get_song(self, music_id: int) -> Optional[Row]:
sql = music.select(music.c.id == music_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def put_avatar( def put_avatar(
self, self,
version: int, version: int,

View File

@@ -11,24 +11,6 @@ class ChuniSun(ChuniNewPlus):
super().__init__(core_cfg, game_cfg) super().__init__(core_cfg, game_cfg)
self.version = ChuniConstants.VER_CHUNITHM_SUN self.version = ChuniConstants.VER_CHUNITHM_SUN
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
ret = super().handle_get_game_setting_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/210/ChuniServlet/"
ret["gameSetting"][
"matchingUriX"
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/210/ChuniServlet/"
ret["gameSetting"][
"udpHolePunchUri"
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/210/ChuniServlet/"
ret["gameSetting"][
"reflectorUri"
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/210/ChuniServlet/"
return ret
def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
user_data = super().handle_cm_get_user_preview_api_request(data) user_data = super().handle_cm_get_user_preview_api_request(data)

19
titles/chuni/sunplus.py Normal file
View File

@@ -0,0 +1,19 @@
from typing import Dict, Any
from core.config import CoreConfig
from titles.chuni.sun import ChuniSun
from titles.chuni.const import ChuniConstants
from titles.chuni.config import ChuniConfig
class ChuniSunPlus(ChuniSun):
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
super().__init__(core_cfg, game_cfg)
self.version = ChuniConstants.VER_CHUNITHM_SUN_PLUS
def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
user_data = super().handle_cm_get_user_preview_api_request(data)
# I don't know if lastDataVersion is going to matter, I don't think CardMaker 1.35 works this far up
user_data["lastDataVersion"] = "2.15.00"
return user_data

View File

@@ -4,7 +4,9 @@ import json
import logging import logging
from enum import Enum from enum import Enum
import pytz
from core.config import CoreConfig from core.config import CoreConfig
from core.utils import Utils
from core.data.cache import cached from core.data.cache import cached
from titles.cm.const import CardMakerConstants from titles.cm.const import CardMakerConstants
from titles.cm.config import CardMakerConfig from titles.cm.config import CardMakerConfig
@@ -28,8 +30,8 @@ class CardMakerBase:
return version.replace(".", "")[:3] return version.replace(".", "")[:3]
def handle_get_game_connect_api_request(self, data: Dict) -> Dict: def handle_get_game_connect_api_request(self, data: Dict) -> Dict:
if self.core_cfg.server.is_develop: if not self.core_cfg.server.is_using_proxy and Utils.get_title_port(self.core_cfg) != 80:
uri = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}" uri = f"http://{self.core_cfg.title.hostname}:{Utils.get_title_port(self.core_cfg)}"
else: else:
uri = f"http://{self.core_cfg.title.hostname}" uri = f"http://{self.core_cfg.title.hostname}"
@@ -43,13 +45,13 @@ class CardMakerBase:
{ {
"modelKind": 0, "modelKind": 0,
"type": 1, "type": 1,
"titleUri": f"{uri}/SDHD/{self._parse_int_ver(games_ver['chuni'])}/", "titleUri": f"{uri}/SDHD/{self._parse_int_ver(games_ver['chuni'])}/ChuniServlet/",
}, },
# maimai DX # maimai DX
{ {
"modelKind": 1, "modelKind": 1,
"type": 1, "type": 1,
"titleUri": f"{uri}/SDEZ/{self._parse_int_ver(games_ver['maimai'])}/", "titleUri": f"{uri}/{self._parse_int_ver(games_ver['maimai'])}/Maimai2Servlet/",
}, },
# ONGEKI # ONGEKI
{ {
@@ -61,12 +63,29 @@ class CardMakerBase:
} }
def handle_get_game_setting_api_request(self, data: Dict) -> Dict: def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
reboot_start = date.strftime( # if reboot start/end time is not defined use the default behavior of being a few hours ago
datetime.now() + timedelta(hours=3), self.date_time_format if self.core_cfg.title.reboot_start_time == "" or self.core_cfg.title.reboot_end_time == "":
reboot_start = datetime.strftime(
datetime.utcnow() + timedelta(hours=6), self.date_time_format
) )
reboot_end = date.strftime( reboot_end = datetime.strftime(
datetime.now() + timedelta(hours=4), self.date_time_format 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)
# grab the dict with all games version numbers from user config # grab the dict with all games version numbers from user config
games_ver = self.game_cfg.version.version(self.version) games_ver = self.game_cfg.version.version(self.version)

View File

@@ -7,21 +7,22 @@ import coloredlogs
import zlib import zlib
from os import path from os import path
from typing import Tuple from typing import Tuple, List, Dict
from twisted.web.http import Request from twisted.web.http import Request
from logging.handlers import TimedRotatingFileHandler from logging.handlers import TimedRotatingFileHandler
from core.config import CoreConfig from core.config import CoreConfig
from core.utils import Utils from core.utils import Utils
from titles.cm.config import CardMakerConfig from core.title import BaseServlet
from titles.cm.const import CardMakerConstants from .config import CardMakerConfig
from titles.cm.base import CardMakerBase from .const import CardMakerConstants
from titles.cm.cm135 import CardMaker135 from .base import CardMakerBase
from .cm135 import CardMaker135
class CardMakerServlet: class CardMakerServlet(BaseServlet):
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 = CardMakerConfig() self.game_cfg = CardMakerConfig()
if path.exists(f"{cfg_dir}/{CardMakerConstants.CONFIG_NAME}"): if path.exists(f"{cfg_dir}/{CardMakerConstants.CONFIG_NAME}"):
self.game_cfg.update( self.game_cfg.update(
@@ -57,9 +58,9 @@ class CardMakerServlet:
) )
@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 = CardMakerConfig() game_cfg = CardMakerConfig()
if path.exists(f"{cfg_dir}/{CardMakerConstants.CONFIG_NAME}"): if path.exists(f"{cfg_dir}/{CardMakerConstants.CONFIG_NAME}"):
game_cfg.update( game_cfg.update(
@@ -67,22 +68,21 @@ class CardMakerServlet:
) )
if not game_cfg.server.enable: if not game_cfg.server.enable:
return (False, "", "") return False
if core_cfg.server.is_develop: return True
def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]:
return ( return (
True, [],
f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/", [("render_POST", "/SDED/{version}/{endpoint}", {})]
"",
) )
return (True, f"http://{core_cfg.title.hostname}/{game_code}/$v/", "") def render_POST(self, request: Request, game_code: str, matchers: Dict) -> bytes:
version = int(matchers['version'])
def render_POST(self, request: Request, version: int, url_path: str) -> bytes: endpoint = matchers['endpoint']
req_raw = request.content.getvalue() req_raw = request.content.getvalue()
url_split = url_path.split("/")
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 >= 130 and version < 135: # Card Maker if version >= 130 and version < 135: # Card Maker

View File

@@ -205,6 +205,7 @@ class CardMakerReader(BaseReader):
"1.20": Mai2Constants.VER_MAIMAI_DX_UNIVERSE, "1.20": Mai2Constants.VER_MAIMAI_DX_UNIVERSE,
"1.25": Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS, "1.25": Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS,
"1.30": Mai2Constants.VER_MAIMAI_DX_FESTIVAL, "1.30": Mai2Constants.VER_MAIMAI_DX_FESTIVAL,
"1.35": Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS,
} }
for root, dirs, files in os.walk(base_dir): for root, dirs, files in os.walk(base_dir):

View File

@@ -3,13 +3,12 @@ import json
from decimal import Decimal from decimal import Decimal
from base64 import b64encode from base64 import b64encode
from typing import Any, Dict, List from typing import Any, Dict, List
from hashlib import md5 from os import path
from datetime import datetime
from core.config import CoreConfig from core.config import CoreConfig
from titles.cxb.config import CxbConfig from .config import CxbConfig
from titles.cxb.const import CxbConstants from .const import CxbConstants
from titles.cxb.database import CxbData from .database import CxbData
from threading import Thread from threading import Thread
@@ -22,6 +21,13 @@ class CxbBase:
self.logger = logging.getLogger("cxb") self.logger = logging.getLogger("cxb")
self.version = CxbConstants.VER_CROSSBEATS_REV self.version = CxbConstants.VER_CROSSBEATS_REV
def _get_data_contents(self, folder: str, filetype: str, encoding: str = None, subfolder: str = "") -> List[str]:
if path.exists(f"titles/cxb/data/{folder}/{subfolder}{filetype}.csv"):
with open(f"titles/cxb/data/{folder}/{subfolder}{filetype}.csv", encoding=encoding) as f:
return f.readlines()
return []
def handle_action_rpreq_request(self, data: Dict) -> Dict: def handle_action_rpreq_request(self, data: Dict) -> Dict:
return {} return {}
@@ -192,14 +198,6 @@ class CxbBase:
).decode("utf-8") ).decode("utf-8")
) )
def task_generateIndexData(versionindex):
try:
v_profile = self.data.profile.get_profile_index(0, uid, self.version)
v_profile_data = v_profile["data"]
versionindex.append(int(v_profile_data["appVersion"]))
except Exception:
versionindex.append("10400")
def handle_action_loadrange_request(self, data: Dict) -> Dict: def handle_action_loadrange_request(self, data: Dict) -> Dict:
range_start = data["loadrange"]["range"][0] range_start = data["loadrange"]["range"][0]
range_end = data["loadrange"]["range"][1] range_end = data["loadrange"]["range"][1]
@@ -273,9 +271,14 @@ class CxbBase:
thread_ScoreData = Thread(target=CxbBase.task_generateScoreData(song, index, data1)) thread_ScoreData = Thread(target=CxbBase.task_generateScoreData(song, index, data1))
thread_ScoreData.start() thread_ScoreData.start()
for v in index: v_profile = self.data.profile.get_profile_index(0, uid, self.version)
thread_IndexData = Thread(target=CxbBase.task_generateIndexData(versionindex)) v_profile_data = v_profile["data"]
thread_IndexData.start()
for _, data in enumerate(profile):
if v_profile_data:
versionindex.append(int(v_profile_data["appVersion"]))
else:
versionindex.append("10400")
return {"index": index, "data": data1, "version": versionindex} return {"index": index, "data": data1, "version": versionindex}
@@ -530,7 +533,6 @@ class CxbBase:
profile = self.data.profile.get_profile_index(0, uid, self.version) profile = self.data.profile.get_profile_index(0, uid, self.version)
data1 = profile["data"] data1 = profile["data"]
p = self.data.item.get_energy(uid) p = self.data.item.get_energy(uid)
energy = p["energy"]
if not p: if not p:
self.data.item.put_energy(uid, 5) self.data.item.put_energy(uid, 5)
@@ -543,6 +545,7 @@ class CxbBase:
} }
array = [] array = []
energy = p["energy"]
newenergy = int(energy) + 5 newenergy = int(energy) + 5
self.data.item.put_energy(uid, newenergy) self.data.item.put_energy(uid, newenergy)

View File

@@ -19,42 +19,6 @@ class CxbServerConfig:
) )
) )
@property
def hostname(self) -> str:
return CoreConfig.get_config_field(
self.__config, "cxb", "server", "hostname", default="localhost"
)
@property
def ssl_enable(self) -> bool:
return CoreConfig.get_config_field(
self.__config, "cxb", "server", "ssl_enable", default=False
)
@property
def port(self) -> int:
return CoreConfig.get_config_field(
self.__config, "cxb", "server", "port", default=8082
)
@property
def port_secure(self) -> int:
return CoreConfig.get_config_field(
self.__config, "cxb", "server", "port_secure", default=443
)
@property
def ssl_cert(self) -> str:
return CoreConfig.get_config_field(
self.__config, "cxb", "server", "ssl_cert", default="cert/title.crt"
)
@property
def ssl_key(self) -> str:
return CoreConfig.get_config_field(
self.__config, "cxb", "server", "ssl_key", default="cert/title.key"
)
class CxbConfig(dict): class CxbConfig(dict):
def __init__(self) -> None: def __init__(self) -> None:

Some files were not shown because too many files have changed in this diff Show More