From e0265485ffbbdcdce7d83ec93891a64e9f95f977 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Sun, 1 Oct 2023 03:54:23 +0200 Subject: [PATCH 001/101] Initial D THE ARCADE S2 support added --- docs/game_specific_info.md | 144 +- example_config/idac.yaml | 22 + readme.md | 3 + titles/idac/__init__.py | 12 + titles/idac/base.py | 16 + titles/idac/config.py | 121 + titles/idac/const.py | 16 + titles/idac/data/avatarGacha.json | Bin 0 -> 262275 bytes titles/idac/data/create_delivery_images.py | 36 + .../data/stamps/touhou_flandre_scarlet.json | Bin 0 -> 10735 bytes .../data/stamps/touhou_remilia_scarlet.json | Bin 0 -> 10669 bytes .../data/stamps/touhou_sakuya_izayoi.json | Bin 0 -> 10622 bytes titles/idac/data/timeRelease_v0100.json | Bin 0 -> 1286433 bytes titles/idac/data/timeRelease_v0131.json | Bin 0 -> 1863979 bytes titles/idac/data/timeRelease_v0141.json | Bin 0 -> 1864737 bytes titles/idac/data/timeRelease_v0150.json | Bin 0 -> 1354135 bytes .../timetrial/touhou_remilia_scarlet.json | Bin 0 -> 1131 bytes titles/idac/database.py | 12 + titles/idac/echo.py | 64 + titles/idac/frontend.py | 140 ++ titles/idac/frontend/idac_index.jinja | 132 ++ titles/idac/frontend/js/idac_scripts.js | 10 + titles/idac/index.py | 166 ++ titles/idac/matching.py | 72 + titles/idac/read.py | 161 ++ titles/idac/schema/item.py | 964 ++++++++ titles/idac/schema/profile.py | 440 ++++ titles/idac/season2.py | 2106 +++++++++++++++++ 28 files changed, 4608 insertions(+), 29 deletions(-) create mode 100644 example_config/idac.yaml create mode 100644 titles/idac/__init__.py create mode 100644 titles/idac/base.py create mode 100644 titles/idac/config.py create mode 100644 titles/idac/const.py create mode 100644 titles/idac/data/avatarGacha.json create mode 100644 titles/idac/data/create_delivery_images.py create mode 100644 titles/idac/data/stamps/touhou_flandre_scarlet.json create mode 100644 titles/idac/data/stamps/touhou_remilia_scarlet.json create mode 100644 titles/idac/data/stamps/touhou_sakuya_izayoi.json create mode 100644 titles/idac/data/timeRelease_v0100.json create mode 100644 titles/idac/data/timeRelease_v0131.json create mode 100644 titles/idac/data/timeRelease_v0141.json create mode 100644 titles/idac/data/timeRelease_v0150.json create mode 100644 titles/idac/data/timetrial/touhou_remilia_scarlet.json create mode 100644 titles/idac/database.py create mode 100644 titles/idac/echo.py create mode 100644 titles/idac/frontend.py create mode 100644 titles/idac/frontend/idac_index.jinja create mode 100644 titles/idac/frontend/js/idac_scripts.js create mode 100644 titles/idac/index.py create mode 100644 titles/idac/matching.py create mode 100644 titles/idac/read.py create mode 100644 titles/idac/schema/item.py create mode 100644 titles/idac/schema/profile.py create mode 100644 titles/idac/season2.py diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index 8f52807..2baa12c 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -16,6 +16,7 @@ using the megaime database. Clean installations always create the latest databas - [Card Maker](#card-maker) - [WACCA](#wacca) - [Sword Art Online Arcade](#sao) + - [Initial D THE ARCADE](#initial-d-the-arcade) # Supported Games @@ -27,7 +28,7 @@ Games listed below have been tested and confirmed working. ### SDBT | Version ID | Version Name | -|------------|-----------------------| +| ---------- | --------------------- | | 0 | CHUNITHM | | 1 | CHUNITHM PLUS | | 2 | CHUNITHM AIR | @@ -43,7 +44,7 @@ Games listed below have been tested and confirmed working. ### SDHD/SDBT | Version ID | Version Name | -|------------|---------------------| +| ---------- | ------------------- | | 11 | CHUNITHM NEW!! | | 12 | CHUNITHM NEW PLUS!! | | 13 | CHUNITHM SUN | @@ -94,7 +95,7 @@ After a failed Online Battle the room will be deleted. The host is used for the ### SDCA | Version ID | Version Name | -|------------|------------------------------------| +| ---------- | ---------------------------------- | | 0 | crossbeats REV. | | 1 | crossbeats REV. SUNRISE | | 2 | crossbeats REV. SUNRISE S2 | @@ -114,26 +115,26 @@ The importer for crossbeats REV. will import Music. 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) | +| 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 ### SDEZ -| 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 | 1 | maimai PLUS | | SBZF | 2 | maimai GreeN | @@ -186,7 +187,7 @@ Pre-Dx uses the same database as DX, so only upgrade using the SDEZ game code! ### SBZV | Version ID | Version Name | -|------------|---------------------------------| +| ---------- | ------------------------------- | | 0 | Project Diva Arcade | | 1 | Project Diva Arcade Future Tone | @@ -207,7 +208,7 @@ the Shop, Modules and Customizations. Config file is located in `config/diva.yaml`. | Option | Info | -|----------------------|-------------------------------------------------------------------------------------------------| +| -------------------- | ----------------------------------------------------------------------------------------------- | | `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 | @@ -227,7 +228,7 @@ python dbutils.py --game SBZV upgrade ### SDDT | Version ID | Version Name | -|------------|----------------------------| +| ---------- | -------------------------- | | 0 | O.N.G.E.K.I. | | 1 | O.N.G.E.K.I. + | | 2 | O.N.G.E.K.I. SUMMER | @@ -255,7 +256,7 @@ The importer for O.N.G.E.K.I. will all all Cards, Music and Events. Config file is located in `config/ongeki.yaml`. | 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 | Note: 1149 and higher are only for Card Maker 1.35 and higher and will be ignored on lower versions. @@ -275,7 +276,7 @@ python dbutils.py --game SDDT upgrade ### SDED | Version ID | Version Name | -|------------|-----------------| +| ---------- | --------------- | | 0 | Card Maker 1.30 | | 1 | Card Maker 1.35 | @@ -391,7 +392,7 @@ Gacha IDs up to 1140 will be loaded for CM 1.34 and all gachas will be loaded fo ### SDFE | Version ID | Version Name | -|------------|---------------| +| ---------- | ------------- | | 0 | WACCA | | 1 | WACCA S | | 2 | WACCA Lily | @@ -414,7 +415,7 @@ The importer for WACCA will import all Music data. Config file is located in `config/wacca.yaml`. | Option | Info | -|--------------------|-----------------------------------------------------------------------------| +| ------------------ | --------------------------------------------------------------------------- | | `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_wp` | Sets the user WP to `999999` | @@ -468,9 +469,9 @@ Below is a list of VIP rewards. Currently, VIP is not implemented, and thus thes ### SDEW -| Version ID | Version Name | -|------------|---------------| -| 0 | SAO | +| Version ID | Version Name | +| ---------- | ------------ | +| 0 | SAO | ### Importer @@ -487,11 +488,11 @@ The importer for SAO will import all items, heroes, support skills and titles da Config file is located in `config/sao.yaml`. -| Option | Info | -|--------------------|-----------------------------------------------------------------------------| -| `hostname` | Changes the server listening address for Mucha | -| `port` | Changes the listing port | -| `auto_register` | Allows the game to handle the automatic registration of new cards | +| Option | Info | +| --------------- | ----------------------------------------------------------------- | +| `hostname` | Changes the server listening address for Mucha | +| `port` | Changes the listing port | +| `auto_register` | Allows the game to handle the automatic registration of new cards | ### Database upgrade @@ -513,4 +514,89 @@ python dbutils.py --game SDEW upgrade - Midorica - Limited Network Support - Dniel97 - Helping with network base -- tungnotpunk - Source \ No newline at end of file +- 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 --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.enabled` | 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.enabled` | 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 + +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. Full Tune Ticket +26. Paper Cup +27. BGM +28. Drifting Text +31. Start Menu BG +32. Car Color/Paint +33. Aura Level? +34. Full Tune Ticket Fragment +35. Underneon Lights + +### Credits + +A huge thanks to all people who helped shaping this project to what it is now and don't want to be mentioned here. diff --git a/example_config/idac.yaml b/example_config/idac.yaml new file mode 100644 index 0000000..04ea1e0 --- /dev/null +++ b/example_config/idac.yaml @@ -0,0 +1,22 @@ +server: + enabled: True + loglevel: "debug" + 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: + enabled: True + enabled_stamps: # max 3 play stamps + - "touhou_remilia_scarlet" + - "touhou_flandre_scarlet" + - "touhou_sakuya_izayoi" + +timetrial: + enabled: True + enabled_timetrial: "touhou_remilia_scarlet" diff --git a/readme.md b/readme.md index 2c49faa..0875b55 100644 --- a/readme.md +++ b/readme.md @@ -33,6 +33,9 @@ Games listed below have been tested and confirmed working. Only game versions ol + Sword Art Online Arcade (partial support) + Final ++ Initial D THE ARCADE + + Season 2 + ## Requirements - python 3 (tested working with 3.9 and 3.10, other versions YMMV) - pip diff --git a/titles/idac/__init__.py b/titles/idac/__init__.py new file mode 100644 index 0000000..0c632bd --- /dev/null +++ b/titles/idac/__init__.py @@ -0,0 +1,12 @@ +from titles.idac.index import IDACServlet +from titles.idac.const import IDACConstants +from titles.idac.database import IDACData +from titles.idac.read import IDACReader +from titles.idac.frontend import IDACFrontend + +index = IDACServlet +database = IDACData +reader = IDACReader +frontend = IDACFrontend +game_codes = [IDACConstants.GAME_CODE] +current_schema_version = 1 diff --git a/titles/idac/base.py b/titles/idac/base.py new file mode 100644 index 0000000..c556aff --- /dev/null +++ b/titles/idac/base.py @@ -0,0 +1,16 @@ +import logging + +from core.config import CoreConfig +from titles.idac.config import IDACConfig +from titles.idac.const import IDACConstants +from titles.idac.database import IDACData + + +class IDACBase: + def __init__(self, cfg: CoreConfig, game_cfg: IDACConfig) -> None: + self.core_cfg = cfg + self.game_config = game_cfg + self.game = IDACConstants.GAME_CODE + self.version = IDACConstants.VER_IDAC_SEASON_1 + self.data = IDACData(cfg) + self.logger = logging.getLogger("idac") diff --git a/titles/idac/config.py b/titles/idac/config.py new file mode 100644 index 0000000..e685e2e --- /dev/null +++ b/titles/idac/config.py @@ -0,0 +1,121 @@ +from core.config import CoreConfig + + +class IDACServerConfig: + def __init__(self, parent: "IDACConfig") -> None: + self.__config = parent + + @property + def enable(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "idac", "server", "enable", default=True + ) + + @property + def loglevel(self) -> int: + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "idac", "server", "loglevel", default="info" + ) + ) + + @property + def ssl(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "idac", "server", "ssl", default=False + ) + + @property + def ssl_cert(self) -> str: + return CoreConfig.get_config_field( + self.__config, "idac", "server", "ssl_cert", default="cert/title.crt" + ) + + @property + def ssl_key(self) -> str: + return CoreConfig.get_config_field( + self.__config, "idac", "server", "ssl_key", default="cert/title.key" + ) + + @property + def matching_host(self) -> str: + return CoreConfig.get_config_field( + self.__config, "idac", "server", "matching_host", default="127.0.0.1" + ) + + @property + def matching(self) -> int: + return CoreConfig.get_config_field( + self.__config, "idac", "server", "port_matching", default=20000 + ) + + @property + def echo1(self) -> int: + return CoreConfig.get_config_field( + self.__config, "idac", "server", "port_echo1", default=20001 + ) + + @property + def echo2(self) -> int: + return CoreConfig.get_config_field( + self.__config, "idac", "server", "port_echo2", default=20002 + ) + + @property + def matching_p2p(self) -> int: + return CoreConfig.get_config_field( + self.__config, "idac", "server", "port_matching_p2p", default=20003 + ) + + +class IDACStampConfig: + def __init__(self, parent: "IDACConfig") -> None: + self.__config = parent + + @property + def enable(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "idac", "stamp", "enable", default=True + ) + + @property + def enabled_stamps(self) -> list: + return CoreConfig.get_config_field( + self.__config, + "idac", + "stamp", + "enabled_stamps", + default=[ + "touhou_remilia_scarlet", + "touhou_flandre_scarlet", + "touhou_sakuya_izayoi", + ], + ) + + +class IDACTimetrialConfig: + def __init__(self, parent: "IDACConfig") -> None: + self.__config = parent + + @property + def enable(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "idac", "timetrial", "enable", default=True + ) + + @property + def enabled_timetrial(self) -> str: + return CoreConfig.get_config_field( + self.__config, + "idac", + "timetrial", + "enabled_stamps", + default="touhou_remilia_scarlet", + ) + + +class IDACConfig(dict): + def __init__(self) -> None: + self.server = IDACServerConfig(self) + self.stamp = IDACStampConfig(self) + self.timetrial = IDACTimetrialConfig(self) diff --git a/titles/idac/const.py b/titles/idac/const.py new file mode 100644 index 0000000..cfae20e --- /dev/null +++ b/titles/idac/const.py @@ -0,0 +1,16 @@ +class IDACConstants(): + GAME_CODE = "SDGT" + + CONFIG_NAME = "idac.yaml" + + VER_IDAC_SEASON_1 = 0 + VER_IDAC_SEASON_2 = 1 + + VERSION_STRING = ( + "Initial D THE ARCADE Season 1", + "Initial D THE ARCADE Season 2", + ) + + @classmethod + def game_ver_to_string(cls, ver: int): + return cls.VERSION_STRING[ver] diff --git a/titles/idac/data/avatarGacha.json b/titles/idac/data/avatarGacha.json new file mode 100644 index 0000000000000000000000000000000000000000..91bb516a9d64ff6a5aa4d1ac400e4214172c0f09 GIT binary patch literal 262275 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 literal 0 HcmV?d00001 diff --git a/titles/idac/data/create_delivery_images.py b/titles/idac/data/create_delivery_images.py new file mode 100644 index 0000000..196081e --- /dev/null +++ b/titles/idac/data/create_delivery_images.py @@ -0,0 +1,36 @@ +import os +import hashlib + + +def prepare_images(image_folder="./images"): + print(f"Preparing image delivery files in {image_folder}...") + + for file in os.listdir(image_folder): + if file.endswith(".png") or file.endswith(".jpg"): + dpg_name = "adv-" + file[:-4].upper() + if file.endswith(".png"): + dpg_name += ".dpg" + else: + dpg_name += ".djg" + + if os.path.exists(os.path.join(image_folder, dpg_name)): + continue + else: + with open( + os.path.join(image_folder, file), "rb" + ) as original_image_file: + original_image = original_image_file.read() + image_hash = hashlib.md5(original_image).hexdigest() + print( + f"DPG for {file} not found, creating with hash {image_hash}..." + ) + md5_buf = bytes.fromhex(image_hash) + dpg_buf = md5_buf + original_image + with open(os.path.join(image_folder, dpg_name), "wb") as dpg_file: + dpg_file.write(dpg_buf) + + print(f"Created {dpg_name}.") + + +# Call the function to execute it +prepare_images() diff --git a/titles/idac/data/stamps/touhou_flandre_scarlet.json b/titles/idac/data/stamps/touhou_flandre_scarlet.json new file mode 100644 index 0000000000000000000000000000000000000000..31638e1c8ecf2d6e1c31b307b4c3193d28e203db GIT binary patch literal 10735 zcmeHNUvJb#5P#=WM1GyT-Z7P@N8ed#seZxIY2lptSqv$S*enHVI6x~2kjH2f#`V)nRD8ygz&wUg{ zDB7ki@2J?Gl4|Ss+C;^=@4MdR`FYz}>t3`S+qaFjX{=uItd?=<8+X3Z&}#F_g*k3Q z&(}>K-&t=q7Z-0TxXL^Ha*zM!iM%sGK zvaSv_t7)@gnq0xx)EMHg>--z_9({toYWP;G+uOj8soMmgU(k2a9nWX)}Gsu>}ef_AKJt!T~ZNr;BA$FS3E* zp|!hRR|kWHVYBhjn={(s=Byi`Rq0vRjGn1un|#}8=^exClLrev6UD*(x`BloDSjXX zSJ3TmxE2*u7nT;XR*Rxs?+Vr{v4N@`L(cYe*IvEacjYRXoNn443m>Cn;hU8A;j<4f z?x5gc@bpmOaR9b&jt;4muN#taa-Wp*wOSLR2kZq{xZW{*coJVr9$(;V6z!t$8BSy8 zNAuoiN4vib-tOfqouPr#0*>^)H%MAIH)MP;Ug5?LLsgd zarL-O{)aDd!u@Nno&)xo@=C%M?ss)TlRz|W*GrHuA{OyGPG&o}i2w4dGn?TAw^o#N zLTdQLNJrLg`*e+l*@xF_d)1;|UBahh>NO>WNQQZ1sU_uw+LRbDd3iCo^~c{oZhhE~ zaUuWyhutK`sZT68lK4hW4&?PDiOG`Hr7*0Da!*n+a`a1qFDX8#(lOQ^lOs=P7u`qU z@3eq?o{N0$o=Sc=RCG#Ql7N^Y<`l4zs4SQgkDdmDcQ?`2OloZuFk zEw^@0nV5pNDoV_Tq|o5ZR6lk_bTaq}?eHh~o_<~){Tz^OmV-Aa+)m<$Igp(E>N-jiORcjK_X=hqVPAJHr(>?X1k!01$TU z%HB0VTglRMDT~~Ho_7nHFe2~^?8{V@cgKtV3R7=3*+Do>M7s1%Dd?&w>1KHr0Akmn z7e&xfYLb)fbMVYW-r>!d20otD?WNw@%witgp2qwnHNAL1prUzhMm50vOsQ7pM2Ik9 zg1yzQ>5}-@Bz}^_kMYJ;65mPUdr5qEaQK>REx&qn^!ht=>ibo7VbzymoY?pA4g>wK zvuqOIo^4qHgW(EH9^S`4;bvjsmQ}vvizYbQzRXs~3OS2t4xL;~O{4APxFeisgm!j3 h&+T>B?G@W!>sb~#D@F;o+U+~!Xy2OA_N;T8{{Ubk1!e#M literal 0 HcmV?d00001 diff --git a/titles/idac/data/stamps/touhou_remilia_scarlet.json b/titles/idac/data/stamps/touhou_remilia_scarlet.json new file mode 100644 index 0000000000000000000000000000000000000000..82eeff481d160bea35ce9a09d96d04112d6b028c GIT binary patch literal 10669 zcmeHN-EP}96u$3M6uR1mwXq#fiF&=g!+;?Wlt^*3+OlBDD}o|`CGIB;=AlD@1sF1< z-7*wy+O*gPti{~zVvlm*WWCBOjHGmAiod#ob=y>~U9ME$kM0fMaT@fF@*a>!Kn{UCgz!foe?a&g!ebykUQU4Q@{+%R z{0d}WiQ8$n>a6mX@4Mc$r6t?hY+kV)+qaFnX>45eteSE4Q+KIS($poaL!aI9b+4OY?XjA11=FzWgVIV|dc(4=4;AC6 z4bw~-_=E$&fV!Fd;;`{2=o^M_)taqs`lwRWD7(o%N@T!$}*eW?TzbgSKee~r<=AHxsTDXXeH(J>9d!?T?mfP ze)|G~&->4N5Ohxuc9p~@a2z{nQdn^=+$)kXC-~&Uy;_>I-3joV26w@c5iX>C4DLX> z5I&?4?VaJ^_wHHum;Rso8Q`brSWEsJA|*{mtVb?2!7&RewX)Dn5y(Vpn)<=0D>7?@ zJnaw^W?N*v=5h)?yK7mefc0YjNq<{DgguCax!`H03ji3EdL6NJ&n@ z%rrq((&RX@Svq+eLYg_!TymHF4WH1M`}cl1gX}b4OjH*rvp6qA>EabbSb}?XS(BJv zH1rbY$3f3l)U6&(2WL{-heYTf!bU^o2AfUUNdl38p!Iuy`&Y*mrvXe+D!Tzm8o~IoXr~9)^d+}Ci z6o>u;o0rv+Br&~csY(28+{}lYXdh0x!kDMlB#^%4TAL!*pG9%P-ds`VHc5h?a88%? zLJUb_N&MELsEQjb>kBEm#RP|r6uryC|8lPD;+LXWyP$GgL|7y!x(>Y!!W+3(aI&3@ zBqp&}bPJ^+#jlli=MnbU=V!?(#IpbejdKgBA?I_ITAsrnLVk+K(CZRzD7yOhXC+b!I0`EEYIg3wYPV6+nLy4+7IbVLy^SsY{KJQ+;4j-wdI#uaqUY`#vd#Hn`-=*{1WjJQTf?|v=l1$$5+5#ym>=+o^)DA|4tTvR1QH(kUU2C@6wn`1+x*he0R8F)` zD<@&3sZz(dqjhu@Y;sMjp|&)qi(kxP#uumXs~Qxp#Qf1h^hw)xcWr|dl&dwaM|@G% z?k2P=uz@r?Lpjq?ZFB2(*A_P!l~Z-oiQGqP8Bj?Yzr1&T_Qm<}3G#kH!4JqkM!~+s z9D;UYFUbf_&IEcz&|wBnM$qfAda0fCN%>*z&H!c}&;^{QfR2Jb@}EICdy9*6Z;$&g z!aoO7@J}$Z!G2BflH?=SBavDhniw|4lv9pmrZzrZZQhvT09ugx6>!Z zDnWrj`+0OHXfj227sJER*Z%pVhv9x7d8gs0zmFfkMBV^-KOqlNo=w4@Ney=~qwuOp z@(Mb}y_d9-r;?E_aE@Y9j{F}{@Hv@04p9I~8zTQXMBcv#J>6Xy^sZr>KqK4%3MP6r6tW;c|vL4%31vPCzGPUV!_eY0npAVg87MpUH|1q!+-7 ztysD%&l>M!MF2no|CyS_^9)%L(0{e_jL}hlN}!u(RO9d~sKx{n6sECEBnupZG&;jd z;|dst4Uym*S-tvVc1vtG6$a z_W=3(=e+=VhwugY;1CaxcQWdIi@YbWwzwF6HzfowbFRsSY+hfC5CL(;q$ArDf1Z(# z^VXuuPY_KR+p=5|?71wOaDl(2=#Oomy`N!Ku}rxMCU}ehLH8Gpe!l3JXMR7A=xRyk z-gwbf>)a^LN8?4)F7Uck(ZMcCyqYhn?pDJb^`+<_kN(QIZONUf6jX7mAWP|uo)bqw zG$YgIrDy|#g(ZAMw^AC#cwX2u9nh?Np28mc{5%s2{vtp@hVk1;I=+@jZywV0=o+2fY44burn}uS X41A}H2xhC@weS_ZZLR4T*Y^GaH*B_i literal 0 HcmV?d00001 diff --git a/titles/idac/data/timeRelease_v0100.json b/titles/idac/data/timeRelease_v0100.json new file mode 100644 index 0000000000000000000000000000000000000000..283693566dbccf0f631adb79e894d266e5c4dc0b GIT binary patch literal 1286433 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 DB4?_a literal 0 HcmV?d00001 diff --git a/titles/idac/data/timeRelease_v0131.json b/titles/idac/data/timeRelease_v0131.json new file mode 100644 index 0000000000000000000000000000000000000000..184e6d272d6b16475ec524dc42e2f9339d2288cf GIT binary patch literal 1863979 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 YjBa1w|7rTI-kf}^pC9Fa{qV>C4*`RJ(f|Me literal 0 HcmV?d00001 diff --git a/titles/idac/data/timeRelease_v0150.json b/titles/idac/data/timeRelease_v0150.json new file mode 100644 index 0000000000000000000000000000000000000000..02d6bf3ec0bdf8cf6f4136a6debb784303ae9950 GIT binary patch literal 1354135 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 literal 0 HcmV?d00001 diff --git a/titles/idac/database.py b/titles/idac/database.py new file mode 100644 index 0000000..dac4556 --- /dev/null +++ b/titles/idac/database.py @@ -0,0 +1,12 @@ +from core.data import Data +from core.config import CoreConfig +from titles.idac.schema.profile import IDACProfileData +from titles.idac.schema.item import IDACItemData + + +class IDACData(Data): + def __init__(self, cfg: CoreConfig) -> None: + super().__init__(cfg) + + self.profile = IDACProfileData(cfg, self.session) + self.item = IDACItemData(cfg, self.session) diff --git a/titles/idac/echo.py b/titles/idac/echo.py new file mode 100644 index 0000000..88151a7 --- /dev/null +++ b/titles/idac/echo.py @@ -0,0 +1,64 @@ +import logging +from random import randbytes +import socket + +from twisted.internet.protocol import DatagramProtocol +from socketserver import BaseRequestHandler, TCPServer +from typing import Tuple + +from core.config import CoreConfig +from titles.idac.config import IDACConfig +from titles.idac.database import IDACData + + +class IDACEchoUDP(DatagramProtocol): + def __init__(self, cfg: CoreConfig, game_cfg: IDACConfig, port: int) -> None: + super().__init__() + self.port = port + self.core_config = cfg + self.game_config = game_cfg + self.logger = logging.getLogger("idac") + + def datagramReceived(self, data, addr): + self.logger.info( + f"UDP Ping from from {addr[0]}:{addr[1]} -> {self.port} - {data.hex()}" + ) + self.transport.write(data, addr) + + +class IDACEchoTCP(BaseRequestHandler): + def __init__( + self, request, client_address, server, cfg: CoreConfig, game_cfg: IDACConfig + ) -> None: + self.core_config = cfg + self.game_config = game_cfg + self.logger = logging.getLogger("idac") + self.data = IDACData(cfg) + super().__init__(request, client_address, server) + + def handle(self): + data = self.request.recv(1024).strip() + self.logger.debug( + f"TCP Ping from {self.client_address[0]}:{self.client_address[1]} -> {self.server.server_address[1]}: {data.hex()}" + ) + self.request.sendall(data) + self.request.shutdown(socket.SHUT_WR) + + +class IDACEchoTCPFactory(TCPServer): + def __init__( + self, + server_address: Tuple[str, int], + RequestHandlerClass, + cfg: CoreConfig, + game_cfg: IDACConfig, + bind_and_activate: bool = ..., + ) -> None: + super().__init__(server_address, RequestHandlerClass, bind_and_activate) + self.core_config = cfg + self.game_config = game_cfg + + def finish_request(self, request, client_address): + self.RequestHandlerClass( + request, client_address, self, self.core_config, self.game_config + ) diff --git a/titles/idac/frontend.py b/titles/idac/frontend.py new file mode 100644 index 0000000..32b1467 --- /dev/null +++ b/titles/idac/frontend.py @@ -0,0 +1,140 @@ +import json +import yaml +import jinja2 +from os import path +from twisted.web.util import redirectTo +from twisted.web.http import Request +from twisted.web.server import Session + +from core.frontend import FE_Base, IUserSession +from core.config import CoreConfig +from titles.idac.database import IDACData +from titles.idac.schema.profile import * +from titles.idac.schema.item import * +from titles.idac.config import IDACConfig +from titles.idac.const import IDACConstants + + +class IDACFrontend(FE_Base): + def __init__( + self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str + ) -> None: + super().__init__(cfg, environment) + self.data = IDACData(cfg) + self.game_cfg = IDACConfig() + if path.exists(f"{cfg_dir}/{IDACConstants.CONFIG_NAME}"): + self.game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{IDACConstants.CONFIG_NAME}")) + ) + self.nav_name = "頭文字D THE ARCADE" + # TODO: Add version list + self.version = IDACConstants.VER_IDAC_SEASON_2 + + self.ticket_names = { + 3: "car_dressup_points", + 5: "avatar_points", + 25: "full_tune_tickets", + 34: "full_tune_fragments", + } + + def generate_all_tables_json(self, user_id: int): + json_export = {} + + idac_tables = { + profile, + config, + avatar, + rank, + stock, + theory, + car, + ticket, + story, + episode, + difficulty, + course, + trial, + challenge, + theory_course, + theory_partner, + theory_running, + vs_info, + stamp, + timetrial_event + } + + for table in idac_tables: + sql = select(table).where( + table.c.user == user_id, + ) + + # check if the table has a version column + if "version" in table.c: + sql = sql.where(table.c.version == self.version) + + # lol use the profile connection for items, dirty hack + result = self.data.profile.execute(sql) + data_list = result.fetchall() + + # add the list to the json export with the correct table name + json_export[table.name] = [] + for data in data_list: + tmp = data._asdict() + tmp.pop("id") + tmp.pop("user") + json_export[table.name].append(tmp) + + return json.dumps(json_export, indent=4, default=str, ensure_ascii=False) + + def render_GET(self, request: Request) -> bytes: + uri: str = request.uri.decode() + + template = self.environment.get_template( + "titles/idac/frontend/idac_index.jinja" + ) + sesh: Session = request.getSession() + usr_sesh = IUserSession(sesh) + + # profile export + if uri.startswith("/game/idac/export"): + if usr_sesh.user_id == 0: + return redirectTo(b"/game/idac", request) + + # set the file name, content type and size to download the json + content = self.generate_all_tables_json(usr_sesh.user_id).encode("utf-8") + request.responseHeaders.addRawHeader( + b"content-type", b"application/octet-stream" + ) + request.responseHeaders.addRawHeader( + b"content-disposition", b"attachment; filename=idac_profile.json" + ) + request.responseHeaders.addRawHeader( + b"content-length", str(len(content)).encode("utf-8") + ) + + self.logger.info(f"User {usr_sesh.user_id} exported their IDAC data") + return content + + profile_data, tickets, rank = None, None, None + if usr_sesh.user_id > 0: + profile_data = self.data.profile.get_profile(usr_sesh.user_id, self.version) + ticket_data = self.data.item.get_tickets(usr_sesh.user_id) + rank = self.data.profile.get_profile_rank(usr_sesh.user_id, self.version) + + tickets = { + self.ticket_names[ticket["ticket_id"]]: ticket["ticket_cnt"] + for ticket in ticket_data + } + + return template.render( + title=f"{self.core_config.server.name} | {self.nav_name}", + game_list=self.environment.globals["game_list"], + profile=profile_data, + tickets=tickets, + rank=rank, + sesh=vars(usr_sesh), + active_page="idac", + ).encode("utf-16") + + def render_POST(self, request: Request) -> bytes: + pass diff --git a/titles/idac/frontend/idac_index.jinja b/titles/idac/frontend/idac_index.jinja new file mode 100644 index 0000000..4bd10ca --- /dev/null +++ b/titles/idac/frontend/idac_index.jinja @@ -0,0 +1,132 @@ +{% extends "core/frontend/index.jinja" %} +{% block content %} +

頭文字D THE ARCADE

+ +{% if sesh is defined and sesh["user_id"] > 0 %} +
+
+
+
+

{{ sesh["username"] }}'s Profile

+
+
+ + +
+
+
+
+ + {% if profile is defined and profile is not none %} +
+
+
+
+
Information
+
+
Username
+

{{ profile.username }}

+
Grade
+

+ {% set grade = rank.grade %} + {% if grade >= 1 and grade <= 72 %} + {% set grade_number = (grade - 1) // 9 %} + {% set grade_letters = ['E', 'D', 'C', 'B', 'A', 'S', 'SS', 'X'] %} + {{ grade_letters[grade_number] }}{{ 9 - ((grade-1) % 9) }} + {% else %} + Unknown + {% endif %} +

+
+
+
+
+
+ +
+
Statistics
+
+
+
+
Total Plays
+

{{ profile.total_play }}

+
+
+
Last Played
+

{{ profile.last_play_date }}

+
+
+
Mileage
+

{{ profile.mileage }} m

+
+
+ {% if tickets is defined and tickets|length > 0 %} +
Tokens/Tickets
+
+
+
+
Avatar Tokens
+

{{ tickets.avatar_points }}/30

+
+
+
Car Dressup Tokens
+

{{ tickets.car_dressup_points }}/30

+
+
+
FullTune Tickets
+

{{ tickets.full_tune_tickets }}/99

+
+
+
FullTune Fragments
+

{{ tickets.full_tune_fragments }}/10

+
+
+ {% endif %} +
+
+
+
+ {% else %} + + {% endif %} + +
+
+{% else %} +
+{% endif %} + + + + + +{% endblock content %} \ No newline at end of file diff --git a/titles/idac/frontend/js/idac_scripts.js b/titles/idac/frontend/js/idac_scripts.js new file mode 100644 index 0000000..111fea6 --- /dev/null +++ b/titles/idac/frontend/js/idac_scripts.js @@ -0,0 +1,10 @@ +$(document).ready(function () { + $('#exportBtn').click(function () { + window.location = "/game/idac/export"; + + // appendAlert('Successfully exported the profile', 'success'); + + // Close the modal on success + $('#export').modal('hide'); + }); +}); \ No newline at end of file diff --git a/titles/idac/index.py b/titles/idac/index.py new file mode 100644 index 0000000..29ecb67 --- /dev/null +++ b/titles/idac/index.py @@ -0,0 +1,166 @@ +import json +import traceback +import inflection +import yaml +import logging +import coloredlogs + +from os import path +from typing import Dict, Tuple +from logging.handlers import TimedRotatingFileHandler +from twisted.web import server +from twisted.web.http import Request +from twisted.internet import reactor, endpoints + +from core.config import CoreConfig +from core.utils import Utils +from titles.idac.base import IDACBase +from titles.idac.season2 import IDACSeason2 +from titles.idac.config import IDACConfig +from titles.idac.const import IDACConstants +from titles.idac.echo import IDACEchoUDP +from titles.idac.matching import IDACMatching + + +class IDACServlet: + def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: + self.core_cfg = core_cfg + self.game_cfg = IDACConfig() + self.game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{IDACConstants.CONFIG_NAME}")) + ) + + self.versions = [ + IDACBase(core_cfg, self.game_cfg), + IDACSeason2(core_cfg, self.game_cfg) + ] + + self.logger = logging.getLogger("idac") + log_fmt_str = "[%(asctime)s] IDAC | %(levelname)s | %(message)s" + log_fmt = logging.Formatter(log_fmt_str) + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.core_cfg.server.log_dir, "idac"), + encoding="utf8", + when="d", + backupCount=10, + ) + + fileHandler.setFormatter(log_fmt) + + consoleHandler = logging.StreamHandler() + consoleHandler.setFormatter(log_fmt) + + self.logger.addHandler(fileHandler) + self.logger.addHandler(consoleHandler) + + self.logger.setLevel(self.game_cfg.server.loglevel) + coloredlogs.install( + level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str + ) + + @classmethod + def get_allnet_info( + cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str + ) -> Tuple[bool, str, str]: + game_cfg = IDACConfig() + + if path.exists(f"{cfg_dir}/{IDACConstants.CONFIG_NAME}"): + game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{IDACConstants.CONFIG_NAME}")) + ) + + if not game_cfg.server.enable: + return (False, "", "") + + if core_cfg.server.is_develop: + return ( + True, + f"", + # requires http or else it defautls to https + f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/", + ) + + return ( + True, + f"", + # requires http or else it defautls to https + f"http://{core_cfg.title.hostname}/{game_code}/$v/", + ) + + def render_POST(self, request: Request, version: int, url_path: str) -> bytes: + req_raw = request.content.getvalue() + url_split = url_path.split("/") + internal_ver = 0 + endpoint = url_split[len(url_split) - 1] + client_ip = Utils.get_ip_addr(request) + + if version >= 100 and version < 140: # IDAC Season 1 + internal_ver = IDACConstants.VER_IDAC_SEASON_1 + elif version >= 140 and version < 171: # IDAC Season 2 + internal_ver = IDACConstants.VER_IDAC_SEASON_2 + + if url_split[0] == "initiald": + header_application = self.decode_header(request.getAllHeaders()) + + req_data = json.loads(req_raw) + + self.logger.info(f"v{version} {endpoint} request from {client_ip}") + self.logger.debug(f"Headers: {header_application}") + self.logger.debug(req_data) + + # func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" + func_to_find = "handle_" + for x in url_split: + func_to_find += f"{x.lower()}_" if not x == "" and not x == "initiald" else "" + func_to_find += f"request" + + if not hasattr(self.versions[internal_ver], func_to_find): + self.logger.warning(f"Unhandled v{version} request {endpoint}") + return '{"status_code": "0"}'.encode("utf-8") + + resp = None + try: + handler = getattr(self.versions[internal_ver], func_to_find) + resp = handler(req_data, header_application) + + except Exception as e: + traceback.print_exc() + self.logger.error(f"Error handling v{version} method {endpoint} - {e}") + return '{"status_code": "0"}'.encode("utf-8") + + if resp is None: + resp = {"status_code": "0"} + + self.logger.debug(f"Response {resp}") + return json.dumps(resp, ensure_ascii=False).encode("utf-8") + + self.logger.warning( + f"IDAC unknown request {url_path} - {request.content.getvalue().decode()}" + ) + return '{"status_code": "0"}'.encode("utf-8") + + def decode_header(self, data: Dict) -> Dict: + app: str = data[b"application"].decode() + ret = {} + + for x in app.split(", "): + y = x.split("=") + ret[y[0]] = y[1].replace('"', "") + + return ret + + def setup(self): + if self.game_cfg.server.enable: + endpoints.serverFromString( + reactor, + f"tcp:{self.game_cfg.server.matching}:interface={self.core_cfg.server.listen_address}", + ).listen(server.Site(IDACMatching(self.core_cfg, self.game_cfg))) + + reactor.listenUDP( + self.game_cfg.server.echo1, + IDACEchoUDP(self.core_cfg, self.game_cfg, self.game_cfg.server.echo1), + ) + reactor.listenUDP( + self.game_cfg.server.echo2, + IDACEchoUDP(self.core_cfg, self.game_cfg, self.game_cfg.server.echo2), + ) diff --git a/titles/idac/matching.py b/titles/idac/matching.py new file mode 100644 index 0000000..fb9f56d --- /dev/null +++ b/titles/idac/matching.py @@ -0,0 +1,72 @@ +import json +import logging + +from typing import Dict +from twisted.web import resource + +from core import CoreConfig +from titles.idac.season2 import IDACBase +from titles.idac.config import IDACConfig + + +class IDACMatching(resource.Resource): + isLeaf = True + + def __init__(self, cfg: CoreConfig, game_cfg: IDACConfig) -> None: + self.core_config = cfg + self.game_config = game_cfg + self.base = IDACBase(cfg, game_cfg) + self.logger = logging.getLogger("idac") + + self.queue = 0 + + def get_matching_state(self): + if self.queue >= 1: + self.queue -= 1 + return 0 + else: + return 1 + + def render_POST(self, req) -> bytes: + url = req.uri.decode() + req_data = json.loads(req.content.getvalue().decode()) + header_application = self.decode_header(req.getAllHeaders()) + user_id = int(header_application["session"]) + + # self.getMatchingStatus(user_id) + + self.logger.info( + f"IDAC Matching request from {req.getClientIP()}: {url} - {req_data}" + ) + + resp = {"status_code": "0"} + if url == "/regist": + self.queue = self.queue + 1 + elif url == "/status": + if req_data.get("cancel_flag"): + self.queue = self.queue - 1 + self.logger.info( + f"IDAC Matching endpoint {req.getClientIP()} had quited" + ) + + resp = { + "status_code": "0", + # Only IPv4 is supported + "host": self.game_config.server.matching_host, + "port": self.game_config.server.matching_p2p, + "room_name": "INDTA", + "state": self.get_matching_state(), + } + + self.logger.debug(f"Response {resp}") + return json.dumps(resp, ensure_ascii=False).encode("utf-8") + + def decode_header(self, data: Dict) -> Dict: + app: str = data[b"application"].decode() + ret = {} + + for x in app.split(", "): + y = x.split("=") + ret[y[0]] = y[1].replace('"', "") + + return ret diff --git a/titles/idac/read.py b/titles/idac/read.py new file mode 100644 index 0000000..8798e9b --- /dev/null +++ b/titles/idac/read.py @@ -0,0 +1,161 @@ +import json +import logging +import os +from typing import Any, Dict, List, Optional + +from read import BaseReader +from core.data import Data +from core.config import CoreConfig +from titles.idac.const import IDACConstants +from titles.idac.database import IDACData +from titles.idac.schema.profile import * +from titles.idac.schema.item import * + + +class IDACReader(BaseReader): + def __init__( + self, + config: CoreConfig, + version: int, + bin_dir: Optional[str], + opt_dir: Optional[str], + extra: Optional[str], + ) -> None: + super().__init__(config, version, bin_dir, opt_dir, extra) + self.card_data = Data(config).card + self.data = IDACData(config) + + try: + self.logger.info( + f"Start importer for {IDACConstants.game_ver_to_string(version)}" + ) + except IndexError: + self.logger.error(f"Invalid Initial D THE ARCADE version {version}") + exit(1) + + def read(self) -> None: + if self.bin_dir is None and self.opt_dir is None: + self.logger.error( + ( + "To import your profile specify the '--optfolder'", + " path to your idac_profile.json file, exiting", + ) + ) + exit(1) + + if self.opt_dir is not None: + if not os.path.exists(self.opt_dir): + self.logger.error( + f"Path to idac_profile.json does not exist: {self.opt_dir}" + ) + exit(1) + + if os.path.isdir(self.opt_dir): + self.opt_dir = os.path.join(self.opt_dir, "idac_profile.json") + + if not os.path.isfile(self.opt_dir) or self.opt_dir[-5:] != ".json": + self.logger.error( + f"Path to idac_profile.json does not exist: {self.opt_dir}" + ) + exit(1) + + self.read_idac_profile(self.opt_dir) + + def read_idac_profile(self, file_path: str) -> None: + self.logger.info(f"Reading profile from {file_path}...") + + # read it as binary to avoid encoding issues + profile_data: Dict[str, Any] = {} + with open(file_path, "rb") as f: + profile_data = json.loads(f.read().decode("utf-8")) + + if not profile_data: + self.logger.error("Profile could not be parsed, exiting") + exit(1) + + access_code = None + while access_code is None: + access_code = input("Enter your 20 digits access code: ") + if len(access_code) != 20 or not access_code.isdigit(): + access_code = None + self.logger.warning("Invalid access code, please try again.") + + # check if access code already exists, if not create a new profile + user_id = self.card_data.get_user_id_from_card(access_code) + if user_id is None: + choice = input("Access code does not exist, do you want to create a new profile? (Y/n): ") + if choice.lower() == "n": + self.logger.info("Exiting...") + exit(0) + + user_id = self.data.user.create_user() + + if user_id is None: + self.logger.error("Failed to register user!") + user_id = -1 + + else: + card_id = self.data.card.create_card(user_id, access_code) + + if card_id is None: + self.logger.error("Failed to register card!") + user_id = -1 + + if user_id == -1: + self.logger.error("Failed to create profile, exiting") + exit(1) + + # table mapping to insert the data properly + tables = { + "idac_profile": profile, + "idac_profile_config": config, + "idac_profile_avatar": avatar, + "idac_profile_rank": rank, + "idac_profile_stock": stock, + "idac_profile_theory": theory, + "idac_user_car": car, + "idac_user_ticket": ticket, + "idac_user_story": story, + "idac_user_story_episode": episode, + "idac_user_story_episode_difficulty": difficulty, + "idac_user_course": course, + "idac_user_time_trial": trial, + "idac_user_challenge": challenge, + "idac_user_theory_course": theory_course, + "idac_user_theory_partner": theory_partner, + "idac_user_theory_running": theory_running, + "idac_user_vs_info": vs_info, + "idac_user_stamp": stamp, + "idac_user_timetrial_event": timetrial_event, + } + + for name, data_list in profile_data.items(): + # get the SQLAlchemy table object from the name + table = tables.get(name) + if table is None: + self.logger.warning(f"Unknown table {name}, skipping") + continue + + for data in data_list: + # add user to the data + data["user"] = user_id + + # check if the table has a version column + if "version" in table.c: + data["version"] = self.version + + sql = insert(table).values( + **data + ) + + # lol use the profile connection for items, dirty hack + conflict = sql.on_duplicate_key_update(**data) + result = self.data.profile.execute(conflict) + + if result is None: + self.logger.error(f"Failed to insert data into table {name}") + exit(1) + + self.logger.info(f"Inserted data into table {name}") + + self.logger.info("Profile import complete!") diff --git a/titles/idac/schema/item.py b/titles/idac/schema/item.py new file mode 100644 index 0000000..ae49c74 --- /dev/null +++ b/titles/idac/schema/item.py @@ -0,0 +1,964 @@ +from typing import Dict, Optional, List +from sqlalchemy import ( + Table, + Column, + UniqueConstraint, + PrimaryKeyConstraint, + and_, + update, +) +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON +from sqlalchemy.schema import ForeignKey +from sqlalchemy.engine import Row +from sqlalchemy.sql import func, select +from sqlalchemy.dialects.mysql import insert + +from core.data.schema import BaseData, metadata + +car = Table( + "idac_user_car", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("version", Integer, nullable=False), + Column("car_id", Integer), + Column("style_car_id", Integer), + Column("color", Integer), + Column("bureau", Integer), + Column("kana", Integer), + Column("s_no", Integer), + Column("l_no", Integer), + Column("car_flag", Integer), + Column("tune_point", Integer), + Column("tune_level", Integer, server_default="1"), + Column("tune_parts", Integer), + Column("infinity_tune", Integer, server_default="0"), + Column("online_vs_win", Integer, server_default="0"), + Column( + "pickup_seq", Integer, server_default="1" + ), # the order in which the car was picked up + Column( + "purchase_seq", Integer, server_default="1" + ), # the order in which the car was purchased + Column("color_stock_list", String(32)), + Column("color_stock_new_list", String(32)), + Column("parts_stock_list", String(48)), + Column("parts_stock_new_list", String(48)), + Column("parts_set_equip_list", String(48)), + Column("parts_list", JSON), + Column("equip_parts_count", Integer, server_default="0"), + Column("total_car_parts_count", Integer, server_default="0"), + Column("use_count", Integer, server_default="0"), + Column("story_use_count", Integer, server_default="0"), + Column("timetrial_use_count", Integer, server_default="0"), + Column("vs_use_count", Integer, server_default="0"), + Column("net_vs_use_count", Integer, server_default="0"), + Column("theory_use_count", Integer, server_default="0"), + Column("car_mileage", Integer, server_default="0"), + UniqueConstraint("user", "version", "style_car_id", name="idac_user_car_uk"), + mysql_charset="utf8mb4", +) + +ticket = Table( + "idac_user_ticket", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("ticket_id", Integer), + Column("ticket_cnt", Integer), + UniqueConstraint("user", "ticket_id", name="idac_user_ticket_uk"), + mysql_charset="utf8mb4", +) + +story = Table( + "idac_user_story", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("story_type", Integer), + Column("chapter", Integer), + Column("loop_count", Integer, server_default="1"), + UniqueConstraint("user", "chapter", name="idac_user_story_uk"), + mysql_charset="utf8mb4", +) + +episode = Table( + "idac_user_story_episode", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("chapter", Integer), + Column("episode", Integer), + Column("play_status", Integer), + UniqueConstraint("user", "chapter", "episode", name="idac_user_story_episode_uk"), + mysql_charset="utf8mb4", +) + +difficulty = Table( + "idac_user_story_episode_difficulty", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("episode", Integer), + Column("difficulty", Integer), + Column("play_count", Integer), + Column("clear_count", Integer), + Column("play_status", Integer), + Column("play_score", Integer), + UniqueConstraint( + "user", "episode", "difficulty", name="idac_user_story_episode_difficulty_uk" + ), + mysql_charset="utf8mb4", +) + +course = Table( + "idac_user_course", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("course_id", Integer), + Column("run_counts", Integer, server_default="1"), + Column("skill_level_exp", Integer, server_default="0"), + UniqueConstraint("user", "course_id", name="idac_user_course_uk"), + mysql_charset="utf8mb4", +) + +trial = Table( + "idac_user_time_trial", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("version", Integer, nullable=False), + Column("style_car_id", Integer), + Column("course_id", Integer), + Column("eval_id", Integer, server_default="0"), + Column("goal_time", Integer), + Column("section_time_1", Integer), + Column("section_time_2", Integer), + Column("section_time_3", Integer), + Column("section_time_4", Integer), + Column("mission", Integer), + Column("play_dt", TIMESTAMP, server_default=func.now()), + UniqueConstraint( + "user", "version", "course_id", "style_car_id", name="idac_user_time_trial_uk" + ), + mysql_charset="utf8mb4", +) + +challenge = Table( + "idac_user_challenge", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("vs_type", Integer), + Column("play_difficulty", Integer), + Column("cleared_difficulty", Integer), + Column("story_type", Integer), + Column("play_count", Integer, server_default="1"), + Column("weak_difficulty", Integer, server_default="0"), + Column("eval_id", Integer), + Column("advantage", Integer), + Column("sec1_advantage_avg", Integer), + Column("sec2_advantage_avg", Integer), + Column("sec3_advantage_avg", Integer), + Column("sec4_advantage_avg", Integer), + Column("nearby_advantage_rate", Integer), + Column("win_flag", Integer), + Column("result", Integer), + Column("record", Integer), + Column("course_id", Integer), + Column("last_play_course_id", Integer), + Column("style_car_id", Integer), + Column("course_day", Integer), + UniqueConstraint( + "user", "vs_type", "play_difficulty", name="idac_user_challenge_uk" + ), + mysql_charset="utf8mb4", +) + +theory_course = Table( + "idac_user_theory_course", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("course_id", Integer), + Column("max_victory_grade", Integer, server_default="0"), + Column("run_count", Integer, server_default="1"), + Column("powerhouse_lv", Integer), + Column("powerhouse_exp", Integer), + Column("played_powerhouse_lv", Integer), + Column("update_dt", TIMESTAMP, server_default=func.now()), + UniqueConstraint("user", "course_id", name="idac_user_theory_course_uk"), + mysql_charset="utf8mb4", +) + +theory_partner = Table( + "idac_user_theory_partner", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("partner_id", Integer), + Column("fellowship_lv", Integer), + Column("fellowship_exp", Integer), + UniqueConstraint("user", "partner_id", name="idac_user_theory_partner_uk"), + mysql_charset="utf8mb4", +) + +theory_running = Table( + "idac_user_theory_running", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("course_id", Integer), + Column("attack", Integer), + Column("defense", Integer), + Column("safety", Integer), + Column("runaway", Integer), + Column("trick_flag", Integer), + UniqueConstraint("user", "course_id", name="idac_user_theory_running_uk"), + mysql_charset="utf8mb4", +) + +vs_info = Table( + "idac_user_vs_info", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("group_key", String(25)), + Column("win_flg", Integer), + Column("style_car_id", Integer), + Column("course_id", Integer), + Column("course_day", Integer), + Column("players_num", Integer), + Column("winning", Integer), + Column("advantage_1", Integer), + Column("advantage_2", Integer), + Column("advantage_3", Integer), + Column("advantage_4", Integer), + Column("select_course_id", Integer), + Column("select_course_day", Integer), + Column("select_course_random", Integer), + Column("matching_success_sec", Integer), + Column("boost_flag", Integer), + Column("vs_history", Integer), + Column("break_count", Integer), + Column("break_penalty_flag", Integer), + UniqueConstraint("user", "group_key", name="idac_user_vs_info_uk"), + mysql_charset="utf8mb4", +) + +stamp = Table( + "idac_user_stamp", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("m_stamp_event_id", Integer), + Column("select_flag", Integer), + Column("stamp_masu", Integer), + Column("daily_bonus", Integer), + Column("weekly_bonus", Integer), + Column("weekday_bonus", Integer), + Column("weekend_bonus", Integer), + Column("total_bonus", Integer), + Column("day_total_bonus", Integer), + Column("store_battle_bonus", Integer), + Column("story_bonus", Integer), + Column("online_battle_bonus", Integer), + Column("timetrial_bonus", Integer), + Column("fasteststreetlegaltheory_bonus", Integer), + Column("collaboration_bonus", Integer), + Column("add_bonus_daily_flag_1", Integer), + Column("add_bonus_daily_flag_2", Integer), + Column("add_bonus_daily_flag_3", Integer), + Column("create_date_daily", TIMESTAMP, server_default=func.now()), + Column("create_date_weekly", TIMESTAMP, server_default=func.now()), + UniqueConstraint("user", "m_stamp_event_id", name="idac_user_stamp_uk"), + mysql_charset="utf8mb4", +) + +timetrial_event = Table( + "idac_user_timetrial_event", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("timetrial_event_id", Integer), + Column("point", Integer), + UniqueConstraint("user", "timetrial_event_id", name="idac_user_timetrial_event_uk"), + mysql_charset="utf8mb4", +) + + +class IDACItemData(BaseData): + def get_random_user_car(self, aime_id: int, version: int) -> Optional[List[Row]]: + sql = ( + select(car) + .where(and_(car.c.user == aime_id, car.c.version == version)) + .order_by(func.rand()) + .limit(1) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_random_car(self, version: int) -> Optional[List[Row]]: + sql = select(car).where(car.c.version == version).order_by(func.rand()).limit(1) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_car( + self, aime_id: int, version: int, style_car_id: int + ) -> Optional[List[Row]]: + sql = select(car).where( + and_( + car.c.user == aime_id, + car.c.version == version, + car.c.style_car_id == style_car_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_cars( + self, version: int, aime_id: int, only_pickup: bool = False + ) -> Optional[List[Row]]: + if only_pickup: + sql = select(car).where( + and_( + car.c.user == aime_id, + car.c.version == version, + car.c.pickup_seq != 0, + ) + ) + else: + sql = select(car).where( + and_(car.c.user == aime_id, car.c.version == version) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_ticket(self, aime_id: int, ticket_id: int) -> Optional[Row]: + sql = select(ticket).where( + ticket.c.user == aime_id, ticket.c.ticket_id == ticket_id + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_tickets(self, aime_id: int) -> Optional[List[Row]]: + sql = select(ticket).where(ticket.c.user == aime_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_story(self, aime_id: int, chapter_id: int) -> Optional[Row]: + sql = select(story).where( + and_(story.c.user == aime_id, story.c.chapter == chapter_id) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_stories(self, aime_id: int) -> Optional[List[Row]]: + sql = select(story).where(story.c.user == aime_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_story_episodes(self, aime_id: int, chapter_id: int) -> Optional[List[Row]]: + sql = select(episode).where( + and_(episode.c.user == aime_id, episode.c.chapter == chapter_id) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_story_episode(self, aime_id: int, episode_id: int) -> Optional[Row]: + sql = select(episode).where( + and_(episode.c.user == aime_id, episode.c.episode == episode_id) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_story_episode_difficulties( + self, aime_id: int, episode_id: int + ) -> Optional[List[Row]]: + sql = select(difficulty).where( + and_(difficulty.c.user == aime_id, difficulty.c.episode == episode_id) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_courses(self, aime_id: int) -> Optional[List[Row]]: + sql = select(course).where(course.c.user == aime_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_course(self, aime_id: int, course_id: int) -> Optional[Row]: + sql = select(course).where( + and_(course.c.user == aime_id, course.c.course_id == course_id) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_time_trial_courses(self, version: int) -> Optional[List[Row]]: + sql = select(trial.c.course_id).where(trial.c.version == version).distinct() + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_time_trial_user_best_time_by_course_car( + self, version: int, aime_id: int, course_id: int, style_car_id: int + ) -> Optional[Row]: + sql = select(trial).where( + and_( + trial.c.user == aime_id, + trial.c.version == version, + trial.c.course_id == course_id, + trial.c.style_car_id == style_car_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_time_trial_user_best_courses( + self, version: int, aime_id: int + ) -> Optional[List[Row]]: + # get for a given aime_id the best time for each course + subquery = ( + select( + trial.c.version, + func.min(trial.c.goal_time).label("min_goal_time"), + trial.c.course_id, + ) + .where(and_(trial.c.version == version, trial.c.user == aime_id)) + .group_by(trial.c.course_id) + .subquery() + ) + + # now get the full row for each best time + sql = select(trial).where( + and_( + trial.c.version == subquery.c.version, + trial.c.goal_time == subquery.c.min_goal_time, + trial.c.course_id == subquery.c.course_id, + trial.c.user == aime_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_time_trial_best_cars_by_course( + self, version: int, aime_id: int, course_id: int + ) -> Optional[List[Row]]: + subquery = ( + select( + trial.c.version, + func.min(trial.c.goal_time).label("min_goal_time"), + trial.c.style_car_id, + ) + .where( + and_( + trial.c.version == version, + trial.c.user == aime_id, + trial.c.course_id == course_id, + ) + ) + .group_by(trial.c.style_car_id) + .subquery() + ) + + sql = select(trial).where( + and_( + trial.c.version == subquery.c.version, + trial.c.goal_time == subquery.c.min_goal_time, + trial.c.style_car_id == subquery.c.style_car_id, + trial.c.course_id == course_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_time_trial_ranking_by_course( + self, + version: int, + course_id: int, + style_car_id: Optional[int] = None, + limit: Optional[int] = 10, + ) -> Optional[List[Row]]: + # get the top 10 ranking by goal_time for a given course which is grouped by user + subquery = select( + trial.c.version, + trial.c.user, + func.min(trial.c.goal_time).label("min_goal_time"), + ).where(and_(trial.c.version == version, trial.c.course_id == course_id)) + + # if wantd filter only by style_car_id + if style_car_id is not None: + subquery = subquery.where(trial.c.style_car_id == style_car_id) + + subquery = subquery.group_by(trial.c.user).subquery() + + sql = ( + select(trial) + .where( + and_( + trial.c.version == subquery.c.version, + trial.c.user == subquery.c.user, + trial.c.goal_time == subquery.c.min_goal_time, + ), + ) + .order_by(trial.c.goal_time) + ) + + # limit the result if needed + if limit is not None: + sql = sql.limit(limit) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_time_trial_best_ranking_by_course( + self, version: int, aime_id: int, course_id: int + ) -> Optional[Row]: + sql = ( + select(trial) + .where( + and_( + trial.c.version == version, + trial.c.user == aime_id, + trial.c.course_id == course_id, + ), + ) + .order_by(trial.c.goal_time) + .limit(1) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_challenge( + self, aime_id: int, vs_type: int, play_difficulty: int + ) -> Optional[Row]: + sql = select(challenge).where( + and_( + challenge.c.user == aime_id, + challenge.c.vs_type == vs_type, + challenge.c.play_difficulty == play_difficulty, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_challenges(self, aime_id: int) -> Optional[List[Row]]: + sql = select(challenge).where(challenge.c.user == aime_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_best_challenges_by_vs_type( + self, aime_id: int, story_type: int = 4 + ) -> Optional[List[Row]]: + sql = ( + select( + challenge.c.story_type, + challenge.c.vs_type, + func.max(challenge.c.cleared_difficulty).label("max_clear_lv"), + func.max(challenge.c.play_difficulty).label("last_play_lv"), + challenge.c.course_id, + challenge.c.play_count, + ) + .where( + and_(challenge.c.user == aime_id, challenge.c.story_type == story_type) + ) + .group_by(challenge.c.vs_type, challenge.c.course_id, challenge.c.play_count) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_theory_courses(self, aime_id: int) -> Optional[List[Row]]: + sql = select(theory_course).where(theory_course.c.user == aime_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_theory_course_by_powerhouse_lv( + self, aime_id: int, course_id: int, powerhouse_lv: int, count: int = 3 + ) -> Optional[List[Row]]: + sql = ( + select(theory_course) + .where( + and_( + theory_course.c.user != aime_id, + theory_course.c.course_id == course_id, + theory_course.c.powerhouse_lv == powerhouse_lv, + ) + ) + .order_by(func.rand()) + .limit(count) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_theory_course(self, aime_id: int, course_id: int) -> Optional[List[Row]]: + sql = select(theory_course).where( + and_( + theory_course.c.user == aime_id, theory_course.c.course_id == course_id + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_theory_partners(self, aime_id: int) -> Optional[List[Row]]: + sql = select(theory_partner).where(theory_partner.c.user == aime_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_theory_running(self, aime_id: int) -> Optional[List[Row]]: + sql = select(theory_running).where(theory_running.c.user == aime_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_theory_running_by_course( + self, aime_id: int, course_id: int + ) -> Optional[Row]: + sql = select(theory_running).where( + and_( + theory_running.c.user == aime_id, + theory_running.c.course_id == course_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_vs_infos(self, aime_id: int) -> Optional[List[Row]]: + sql = select(vs_info).where(vs_info.c.user == aime_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_stamps(self, aime_id: int) -> Optional[List[Row]]: + sql = select(stamp).where( + and_( + stamp.c.user == aime_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_timetrial_event(self, aime_id: int, timetrial_event_id: int) -> Optional[Row]: + sql = select(timetrial_event).where( + and_( + timetrial_event.c.user == aime_id, + timetrial_event.c.timetrial_event_id == timetrial_event_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def put_car(self, aime_id: int, version: int, car_data: Dict) -> Optional[int]: + car_data["user"] = aime_id + car_data["version"] = version + + sql = insert(car).values(**car_data) + conflict = sql.on_duplicate_key_update(**car_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_car: Failed to update! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_ticket(self, aime_id: int, ticket_data: Dict) -> Optional[int]: + ticket_data["user"] = aime_id + + sql = insert(ticket).values(**ticket_data) + conflict = sql.on_duplicate_key_update(**ticket_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_ticket: Failed to update! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_story(self, aime_id: int, story_data: Dict) -> Optional[int]: + story_data["user"] = aime_id + + sql = insert(story).values(**story_data) + conflict = sql.on_duplicate_key_update(**story_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_story: Failed to update! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_story_episode_play_status( + self, aime_id: int, chapter_id: int, play_status: int = 1 + ) -> Optional[int]: + sql = ( + update(episode) + .where(and_(episode.c.user == aime_id, episode.c.chapter == chapter_id)) + .values(play_status=play_status) + ) + + result = self.execute(sql) + if result is None: + self.logger.warn( + f"put_story_episode_play_status: Failed to update! aime_id: {aime_id}" + ) + return None + return result.lastrowid + + def put_story_episode( + self, aime_id: int, chapter_id: int, episode_data: Dict + ) -> Optional[int]: + episode_data["user"] = aime_id + episode_data["chapter"] = chapter_id + + sql = insert(episode).values(**episode_data) + conflict = sql.on_duplicate_key_update(**episode_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_story_episode: Failed to update! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_story_episode_difficulty( + self, aime_id: int, episode_id: int, difficulty_data: Dict + ) -> Optional[int]: + difficulty_data["user"] = aime_id + difficulty_data["episode"] = episode_id + + sql = insert(difficulty).values(**difficulty_data) + conflict = sql.on_duplicate_key_update(**difficulty_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn( + f"put_story_episode_difficulty: Failed to update! aime_id: {aime_id}" + ) + return None + return result.lastrowid + + def put_course(self, aime_id: int, course_data: Dict) -> Optional[int]: + course_data["user"] = aime_id + + sql = insert(course).values(**course_data) + conflict = sql.on_duplicate_key_update(**course_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_course: Failed to update! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_time_trial( + self, version: int, aime_id: int, time_trial_data: Dict + ) -> Optional[int]: + time_trial_data["user"] = aime_id + time_trial_data["version"] = version + + sql = insert(trial).values(**time_trial_data) + conflict = sql.on_duplicate_key_update(**time_trial_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_time_trial: Failed to update! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_challenge(self, aime_id: int, challenge_data: Dict) -> Optional[int]: + challenge_data["user"] = aime_id + + sql = insert(challenge).values(**challenge_data) + conflict = sql.on_duplicate_key_update(**challenge_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_challenge: Failed to update! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_theory_course( + self, aime_id: int, theory_course_data: Dict + ) -> Optional[int]: + theory_course_data["user"] = aime_id + + sql = insert(theory_course).values(**theory_course_data) + conflict = sql.on_duplicate_key_update(**theory_course_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_theory_course: Failed to update! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_theory_partner( + self, aime_id: int, theory_partner_data: Dict + ) -> Optional[int]: + theory_partner_data["user"] = aime_id + + sql = insert(theory_partner).values(**theory_partner_data) + conflict = sql.on_duplicate_key_update(**theory_partner_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn( + f"put_theory_partner: Failed to update! aime_id: {aime_id}" + ) + return None + return result.lastrowid + + def put_theory_running( + self, aime_id: int, theory_running_data: Dict + ) -> Optional[int]: + theory_running_data["user"] = aime_id + + sql = insert(theory_running).values(**theory_running_data) + conflict = sql.on_duplicate_key_update(**theory_running_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn( + f"put_theory_running: Failed to update! aime_id: {aime_id}" + ) + return None + return result.lastrowid + + def put_vs_info(self, aime_id: int, vs_info_data: Dict) -> Optional[int]: + vs_info_data["user"] = aime_id + + sql = insert(vs_info).values(**vs_info_data) + conflict = sql.on_duplicate_key_update(**vs_info_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_vs_info: Failed to update! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_stamp( + self, aime_id: int, stamp_data: Dict + ) -> Optional[int]: + stamp_data["user"] = aime_id + + sql = insert(stamp).values(**stamp_data) + conflict = sql.on_duplicate_key_update(**stamp_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn( + f"putstamp: Failed to update! aime_id: {aime_id}" + ) + return None + return result.lastrowid + + def put_timetrial_event( + self, aime_id: int, time_trial_event_id: int, point: int + ) -> Optional[int]: + timetrial_event_data = { + "user": aime_id, + "timetrial_event_id": time_trial_event_id, + "point": point, + } + + sql = insert(timetrial_event).values(**timetrial_event_data) + conflict = sql.on_duplicate_key_update(**timetrial_event_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn( + f"put_timetrial_event: Failed to update! aime_id: {aime_id}" + ) + return None + return result.lastrowid diff --git a/titles/idac/schema/profile.py b/titles/idac/schema/profile.py new file mode 100644 index 0000000..5e363ca --- /dev/null +++ b/titles/idac/schema/profile.py @@ -0,0 +1,440 @@ +from typing import Dict, List, Optional +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, BigInteger +from sqlalchemy.engine.base import Connection +from sqlalchemy.schema import ForeignKey +from sqlalchemy.sql import func, select +from sqlalchemy.engine import Row +from sqlalchemy.dialects.mysql import insert + +from core.data.schema import BaseData, metadata +from core.config import CoreConfig + +profile = Table( + "idac_profile", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("version", Integer, nullable=False), + Column("username", String(8)), + Column("country", Integer), + Column("store", Integer), + Column("team_id", Integer, server_default="0"), + Column("total_play", Integer, server_default="0"), + Column("daily_play", Integer, server_default="0"), + Column("day_play", Integer, server_default="0"), + Column("mileage", Integer, server_default="0"), + Column("asset_version", Integer, server_default="1"), + Column("last_play_date", TIMESTAMP, server_default=func.now()), + Column("mytitle_id", Integer, server_default="0"), + Column("mytitle_efffect_id", Integer, server_default="0"), + Column("sticker_id", Integer, server_default="0"), + Column("sticker_effect_id", Integer, server_default="0"), + Column("papercup_id", Integer, server_default="0"), + Column("tachometer_id", Integer, server_default="0"), + Column("aura_id", Integer, server_default="0"), + Column("aura_color_id", Integer, server_default="0"), + Column("aura_line_id", Integer, server_default="0"), + Column("bgm_id", Integer, server_default="0"), + Column("keyholder_id", Integer, server_default="0"), + Column("start_menu_bg_id", Integer, server_default="0"), + Column("use_car_id", Integer, server_default="1"), + Column("use_style_car_id", Integer, server_default="1"), + Column("bothwin_count", Integer, server_default="0"), + Column("bothwin_score", Integer, server_default="0"), + Column("subcard_count", Integer, server_default="0"), + Column("vs_history", Integer, server_default="0"), + Column("stamp_key_assign_0", Integer), + Column("stamp_key_assign_1", Integer), + Column("stamp_key_assign_2", Integer), + Column("stamp_key_assign_3", Integer), + Column("name_change_category", Integer, server_default="0"), + Column("factory_disp", Integer, server_default="0"), + Column("create_date", TIMESTAMP, server_default=func.now()), + Column("cash", Integer, server_default="0"), + Column("dressup_point", Integer, server_default="0"), + Column("avatar_point", Integer, server_default="0"), + Column("total_cash", Integer, server_default="0"), + UniqueConstraint("user", "version", name="idac_profile_uk"), + mysql_charset="utf8mb4", +) + +# No point setting defaults since the game sends everything on profile creation anyway +config = Table( + "idac_profile_config", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("config_id", Integer), + Column("steering_intensity", Integer), + Column("transmission_type", Integer), + Column("default_viewpoint", Integer), + Column("favorite_bgm", Integer), + Column("bgm_volume", Integer), + Column("se_volume", Integer), + Column("master_volume", Integer), + Column("store_battle_policy", Integer), + Column("battle_onomatope_display", Integer), + Column("cornering_guide", Integer), + Column("minimap", Integer), + Column("line_guide", Integer), + Column("ghost", Integer), + Column("race_exit", Integer), + Column("result_skip", Integer), + Column("stamp_select_skip", Integer), + UniqueConstraint("user", name="idac_profile_config_uk"), + mysql_charset="utf8mb4", +) + +# No point setting defaults since the game sends everything on profile creation anyway +avatar = Table( + "idac_profile_avatar", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("sex", Integer), + Column("face", Integer), + Column("eye", Integer), + Column("mouth", Integer), + Column("hair", Integer), + Column("glasses", Integer), + Column("face_accessory", Integer), + Column("body", Integer), + Column("body_accessory", Integer), + Column("behind", Integer), + Column("bg", Integer), + Column("effect", Integer), + Column("special", Integer), + UniqueConstraint("user", name="idac_profile_avatar_uk"), + mysql_charset="utf8mb4", +) + +rank = Table( + "idac_profile_rank", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("version", Integer, nullable=False), + Column("story_rank_exp", Integer, server_default="0"), + Column("story_rank", Integer, server_default="1"), + Column("time_trial_rank_exp", Integer, server_default="0"), + Column("time_trial_rank", Integer, server_default="1"), + Column("online_battle_rank_exp", Integer, server_default="0"), + Column("online_battle_rank", Integer, server_default="1"), + Column("store_battle_rank_exp", Integer, server_default="0"), + Column("store_battle_rank", Integer, server_default="1"), + Column("theory_exp", Integer, server_default="0"), + Column("theory_rank", Integer, server_default="1"), + Column("pride_group_id", Integer, server_default="0"), + Column("pride_point", Integer, server_default="0"), + Column("grade_exp", Integer, server_default="0"), + Column("grade", Integer, server_default="1"), + Column("grade_reward_dist", Integer, server_default="0"), + Column("story_rank_reward_dist", Integer, server_default="0"), + Column("time_trial_rank_reward_dist", Integer, server_default="0"), + Column("online_battle_rank_reward_dist", Integer, server_default="0"), + Column("store_battle_rank_reward_dist", Integer, server_default="0"), + Column("theory_rank_reward_dist", Integer, server_default="0"), + Column("max_attained_online_battle_rank", Integer, server_default="1"), + Column("max_attained_pride_point", Integer, server_default="0"), + Column("is_last_max", Integer, server_default="0"), + UniqueConstraint("user", "version", name="idac_profile_rank_uk"), + mysql_charset="utf8mb4", +) + +stock = Table( + "idac_profile_stock", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("version", Integer, nullable=False), + Column("mytitle_list", String(1024), server_default=""), + Column("mytitle_new_list", String(1024), server_default=""), + Column("avatar_face_list", String(255), server_default=""), + Column("avatar_face_new_list", String(255), server_default=""), + Column("avatar_eye_list", String(255), server_default=""), + Column("avatar_eye_new_list", String(255), server_default=""), + Column("avatar_hair_list", String(255), server_default=""), + Column("avatar_hair_new_list", String(255), server_default=""), + Column("avatar_body_list", String(255), server_default=""), + Column("avatar_body_new_list", String(255), server_default=""), + Column("avatar_mouth_list", String(255), server_default=""), + Column("avatar_mouth_new_list", String(255), server_default=""), + Column("avatar_glasses_list", String(255), server_default=""), + Column("avatar_glasses_new_list", String(255), server_default=""), + Column("avatar_face_accessory_list", String(255), server_default=""), + Column("avatar_face_accessory_new_list", String(255), server_default=""), + Column("avatar_body_accessory_list", String(255), server_default=""), + Column("avatar_body_accessory_new_list", String(255), server_default=""), + Column("avatar_behind_list", String(255), server_default=""), + Column("avatar_behind_new_list", String(255), server_default=""), + Column("avatar_bg_list", String(255), server_default=""), + Column("avatar_bg_new_list", String(255), server_default=""), + Column("avatar_effect_list", String(255), server_default=""), + Column("avatar_effect_new_list", String(255), server_default=""), + Column("avatar_special_list", String(255), server_default=""), + Column("avatar_special_new_list", String(255), server_default=""), + Column("stamp_list", String(255), server_default=""), + Column("stamp_new_list", String(255), server_default=""), + Column("keyholder_list", String(256), server_default=""), + Column("keyholder_new_list", String(256), server_default=""), + Column("papercup_list", String(255), server_default=""), + Column("papercup_new_list", String(255), server_default=""), + Column("tachometer_list", String(255), server_default=""), + Column("tachometer_new_list", String(255), server_default=""), + Column("aura_list", String(255), server_default=""), + Column("aura_new_list", String(255), server_default=""), + Column("aura_color_list", String(255), server_default=""), + Column("aura_color_new_list", String(255), server_default=""), + Column("aura_line_list", String(255), server_default=""), + Column("aura_line_new_list", String(255), server_default=""), + Column("bgm_list", String(255), server_default=""), + Column("bgm_new_list", String(255), server_default=""), + Column("dx_color_list", String(255), server_default=""), + Column("dx_color_new_list", String(255), server_default=""), + Column("start_menu_bg_list", String(255), server_default=""), + Column("start_menu_bg_new_list", String(255), server_default=""), + Column("under_neon_list", String(255), server_default=""), + UniqueConstraint("user", "version", name="idac_profile_stock_uk"), + mysql_charset="utf8mb4", +) + +theory = Table( + "idac_profile_theory", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("version", Integer, nullable=False), + Column("play_count", Integer, server_default="0"), + Column("play_count_multi", Integer, server_default="0"), + Column("partner_id", Integer), + Column("partner_progress", Integer), + Column("partner_progress_score", Integer), + Column("practice_start_rank", Integer, server_default="0"), + Column("general_flag", Integer, server_default="0"), + Column("vs_history", Integer, server_default="0"), + Column("vs_history_multi", Integer, server_default="0"), + Column("win_count", Integer, server_default="0"), + Column("win_count_multi", Integer, server_default="0"), + UniqueConstraint("user", "version", name="idac_profile_theory_uk"), + mysql_charset="utf8mb4", +) + + +class IDACProfileData(BaseData): + def __init__(self, cfg: CoreConfig, conn: Connection) -> None: + super().__init__(cfg, conn) + self.date_time_format_ext = ( + "%Y-%m-%d %H:%M:%S.%f" # needs to be lopped off at [:-5] + ) + self.date_time_format_short = "%Y-%m-%d" + + def get_profile(self, aime_id: int, version: int) -> Optional[Row]: + sql = select(profile).where( + and_( + profile.c.user == aime_id, + profile.c.version == version, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_different_random_profiles( + self, aime_id: int, version: int, count: int = 9 + ) -> Optional[Row]: + sql = ( + select(profile) + .where( + and_( + profile.c.user != aime_id, + profile.c.version == version, + ) + ) + .order_by(func.rand()) + .limit(count) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_profile_config(self, aime_id: int) -> Optional[Row]: + sql = select(config).where( + and_( + config.c.user == aime_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_profile_avatar(self, aime_id: int) -> Optional[Row]: + sql = select(avatar).where( + and_( + avatar.c.user == aime_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_profile_rank(self, aime_id: int, version: int) -> Optional[Row]: + sql = select(rank).where( + and_( + rank.c.user == aime_id, + rank.c.version == version, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_profile_stock(self, aime_id: int, version: int) -> Optional[Row]: + sql = select(stock).where( + and_( + stock.c.user == aime_id, + stock.c.version == version, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_profile_theory(self, aime_id: int, version: int) -> Optional[Row]: + sql = select(theory).where( + and_( + theory.c.user == aime_id, + theory.c.version == version, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def put_profile( + self, aime_id: int, version: int, profile_data: Dict + ) -> Optional[int]: + profile_data["user"] = aime_id + profile_data["version"] = version + + sql = insert(profile).values(**profile_data) + conflict = sql.on_duplicate_key_update(**profile_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_profile: Failed to update! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_profile_config(self, aime_id: int, config_data: Dict) -> Optional[int]: + config_data["user"] = aime_id + + sql = insert(config).values(**config_data) + conflict = sql.on_duplicate_key_update(**config_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn( + f"put_profile_config: Failed to update! aime_id: {aime_id}" + ) + return None + return result.lastrowid + + def put_profile_avatar(self, aime_id: int, avatar_data: Dict) -> Optional[int]: + avatar_data["user"] = aime_id + + sql = insert(avatar).values(**avatar_data) + conflict = sql.on_duplicate_key_update(**avatar_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn( + f"put_profile_avatar: Failed to update! aime_id: {aime_id}" + ) + return None + return result.lastrowid + + def put_profile_rank( + self, aime_id: int, version: int, rank_data: Dict + ) -> Optional[int]: + rank_data["user"] = aime_id + rank_data["version"] = version + + sql = insert(rank).values(**rank_data) + conflict = sql.on_duplicate_key_update(**rank_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_profile_rank: Failed to update! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_profile_stock( + self, aime_id: int, version: int, stock_data: Dict + ) -> Optional[int]: + stock_data["user"] = aime_id + stock_data["version"] = version + + sql = insert(stock).values(**stock_data) + conflict = sql.on_duplicate_key_update(**stock_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_profile_stock: Failed to update! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_profile_theory( + self, aime_id: int, version: int, theory_data: Dict + ) -> Optional[int]: + theory_data["user"] = aime_id + theory_data["version"] = version + + sql = insert(theory).values(**theory_data) + conflict = sql.on_duplicate_key_update(**theory_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn( + f"put_profile_theory: Failed to update! aime_id: {aime_id}" + ) + return None + return result.lastrowid diff --git a/titles/idac/season2.py b/titles/idac/season2.py new file mode 100644 index 0000000..8e548e5 --- /dev/null +++ b/titles/idac/season2.py @@ -0,0 +1,2106 @@ +from datetime import datetime, timedelta +import os +from random import choice +from typing import Any, Dict, List +import json +import logging + +from core.config import CoreConfig +from titles.idac.const import IDACConstants +from titles.idac.config import IDACConfig +from titles.idac.base import IDACBase + + +class IDACSeason2(IDACBase): + def __init__(self, core_cfg: CoreConfig, game_cfg: IDACConfig) -> None: + super().__init__(core_cfg, game_cfg) + self.version = IDACConstants.VER_IDAC_SEASON_2 + + # load the play stamps and timetrial events into memory + self.stamp_info = [] + if self.game_config.stamp.enable: + for stamp in self.game_config.stamp.enabled_stamps: + if not os.path.exists(f"./titles/idac/data/stamps/{stamp}.json"): + self.logger.warning(f"Stamp {stamp} is enabled but does not exist!") + continue + + with open(f"./titles/idac/data/stamps/{stamp}.json", "rb") as f: + self.logger.debug(f"Loading stamp {stamp}") + self.stamp_info.append( + self._fix_dates(json.loads(f.read().decode("utf-8"))) + ) + + self.timetrial_event = {} + self.timetrial_event_id = None + if self.game_config.timetrial.enable: + timetrial = self.game_config.timetrial.enabled_timetrial + if timetrial is not None: + if not os.path.exists(f"./titles/idac/data/timetrial/{timetrial}.json"): + self.logger.warning( + f"Timetrial {timetrial} is enabled but does not exist!" + ) + else: + self.logger.debug(f"Loading timetrial {timetrial}") + with open(f"./titles/idac/data/timetrial/{timetrial}.json", "rb") as f: + self.timetrial_event = self._fix_dates( + json.loads(f.read().decode("utf-8")) + ) + + # required for saving + self.timetrial_event_id = self.timetrial_event.get("timetrial_event_id") + + def handle_alive_get_request(self, data: Dict, headers: Dict): + return { + "status_code": "0", + # 1 = success, 0 = failed + "server_status": 1, + "force_reboot_time": int(datetime.now().timestamp()) - 86400, + } + + def _fix_dates(self, input: dict): + """ + Fix "start_dt" and "end_dt" dates in a JSON file. + """ + output = {} + + self.logger.debug(f"Fixing dates in {type(input)}") + for key, value in input.items(): + if key in {"start_dt", "end_dt"}: + if isinstance(value, str): + value = int(datetime.strptime(value, "%Y-%m-%d").timestamp()) + + output[key] = value + return output + + def handle_boot_getconfigdata_request(self, data: Dict, headers: Dict): + """ + category: + 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 = Full Tune Ticket + 26 = Paper Cup + 27 = BGM + 28 = Drifting Text + 31 = Start Menu BG + 32 = Car Color/Paint + 33 = Aura Level? + 34 = Full Tune Ticket Fragment + 35 = Underneon Lights + """ + version = headers["device_version"] + ver_str = version.replace(".", "")[:3] + + if self.core_cfg.server.is_develop: + domain_api_game = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDGT/{ver_str}/" + else: + domain_api_game = ( + f"http://{self.core_cfg.title.hostname}/SDGT/{ver_str}/" + ) + + return { + "status_code": "0", + "free_continue_enable": 1, + "free_continue_new": 1, + "free_continue_play": 1, + "difference_time_to_jp": 0, + # has to match the game asset version to show theory of street + "asset_version": "1", + # option version? MV01? + "optional_version": "1", + "disconnect_offset": 0, + "boost_balance_version": "0", + "time_release_number": "0", + "play_stamp_enable": 1, + "play_stamp_bonus_coin": 1, + "gacha_chara_needs": 1, + "both_win_system_control": 1, + "subcard_system_congrol": 1, + "server_maintenance_start_hour": 0, + "server_maintenance_start_minutes": 0, + "server_maintenance_end_hour": 0, + "server_maintenance_end_minutes": 0, + "domain_api_game": domain_api_game, + "domain_matching": f"http://{self.core_cfg.title.hostname}:{self.game_config.server.matching}", + "domain_echo1": f"{self.core_cfg.title.hostname}:{self.game_config.server.echo1}", + "domain_echo2": f"{self.core_cfg.title.hostname}:{self.game_config.server.echo2}", + "domain_ping": f"{self.core_cfg.title.hostname}", + "battle_gift_event_master": [], + "round_event": [], + "last_round_event": [], + "last_round_event_ranking": [], + "round_event_exp": [], + "stamp_info": self.stamp_info, + # 0 = use default data, 1+ = server version of timereleasedata response + "timerelease_no": 5, + # 0 = use default data, 1+ = server version of gachadata response + "timerelease_avatar_gacha_no": 5, + "takeover_reward": [], + "subcard_judge": [ + { + "condition_id": 1, + "lower_rank": 0, + "higher_rank": 10, + "condition_start": 2, + "condition_end": 3, + } + ], + "special_promote": [{"counter": 1, "online_rank_id": 1}], + "matching_id": 1, + "matching_group": [ + { + "group_id": 1, + "group_percent": 1, + } + ], + "timetrial_disp_date": int( + datetime.strptime("2023-10-01", "%Y-%m-%d").timestamp() + ), + # price for every car + "buy_car_need_cash": 5000, + # number of buyable shop/customization time limits + "time_extension_limit": 1, + "collabo_id": 0, + "driver_debut_end_date": int( + datetime.strptime("2029-01-01", "%Y-%m-%d").timestamp() + ), + "online_battle_param1": 1, + "online_battle_param2": 1, + "online_battle_param3": 1, + "online_battle_param4": 1, + "online_battle_param5": 1, + "online_battle_param6": 1, + "online_battle_param7": 1, + "online_battle_param8": 1, + "theory_open_version": "1.30", + "theory_close_version": "1.50", + "special_mode_data": { + "start_dt": int( + datetime.strptime("2023-01-01", "%Y-%m-%d").timestamp() + ), + "end_dt": int(datetime.strptime("2029-01-01", "%Y-%m-%d").timestamp()), + "story_type": 4, # touhou special event + }, + "timetrial_event_data": self.timetrial_event, + } + + def handle_boot_bookkeep_request(self, data: Dict, headers: Dict): + pass + + def handle_boot_getgachadata_request(self, data: Dict, headers: Dict): + """ + Reward category types: + 9: Face + 10: Eye + 11: Mouth + 12: Hair + 13: Glasses + 14: Face accessories + 15: Body + 18: Background + """ + + with open("./titles/idac/data/avatarGacha.json") as f: + avatar_gacha_data = json.load(f) + + # avatar_gacha_data = { + # "status_code": "0", + # "avatar_gacha_data": [ + # { + # "avatar_gacha_id": 0, + # "avatar_gacha_nm": "Standard", + # "gacha_type": 0, + # "save_filename": "0", + # "use_ticket_cnt": 1, + # "start_dt": int( + # datetime.strptime("2019-01-01", "%Y-%m-%d").timestamp() + # ), + # "end_dt": int( + # datetime.strptime("2029-01-01", "%Y-%m-%d").timestamp() + # ), + # "gacha_reward": [ + # { + # "reward_id": 117, + # "reward_type": 118, + # "reward_category": 18, + # "rate": 1000, + # "pickup_flag": 0, + # }, + # ], + # } + # ], + # } + + self.logger.debug( + f'Available avatar gacha items: {len(avatar_gacha_data["avatar_gacha_data"][0]["gacha_reward"])}' + ) + + return avatar_gacha_data + + def handle_boot_gettimereleasedata_request(self, data: Dict, headers: Dict): + """ + timerelease_story: + 1 = Story: 1, 2, 3, 4, 5, 6, 7, 8, 9, 19 (Chapter 10), (29 Chapter 11 lol?) + 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) + """ + path = "./titles/idac/data/" + + # 1.00.00 is default + device_version_data = headers.get("device_version", "1.00.00") + device_version = int(device_version_data.replace(".", "")[:-2]) + + timerelease_filename = f"timeRelease_v{device_version:04d}" + timerelease_path = f"{path}{timerelease_filename}.json" + + # if the file doesn't exist, try to find the next lowest version + if not os.path.exists(timerelease_path): + while device_version > 100: + device_version -= 1 + timerelease_filename = f"timeRelease_v{device_version:04d}" + timerelease_path = f"{path}{timerelease_filename}.json" + + # if the file exists, break out of the loop + if os.path.exists(timerelease_path): + break + + self.logger.debug(f"Using time release file: {timerelease_filename}") + # load the time release data + with open(f"{path}{timerelease_filename}.json") as f: + time_release_data = json.load(f) + + return time_release_data + + def handle_advertise_getrankingdata_request(self, data: Dict, headers: Dict): + best_data = [] + for last_update in data.get("last_update_date"): + course_id = last_update.get("course_id") + + ranking = self.data.item.get_time_trial_ranking_by_course( + self.version, course_id + ) + ranking_data = [] + for i, rank in enumerate(ranking): + user_id = rank["user"] + + # get the username, country and store from the profile + profile = self.data.profile.get_profile(user_id, self.version) + + # should never happen + if profile is None: + continue + + ranking_data.append( + { + "course_id": course_id, + "rank": i + 1, + "username": profile["username"], + "value": rank["goal_time"], + # gat the store name from the profile + "store": self.core_cfg.server.name, + # get the country id from the profile, 9 is JPN + "country": 9, + "style_car_id": rank["style_car_id"], + # convert the dateimt to a timestamp + "play_dt": int(rank["play_dt"].timestamp()), + "section_time_1": rank["section_time_1"], + "section_time_2": rank["section_time_2"], + "section_time_3": rank["section_time_3"], + "section_time_4": rank["section_time_4"], + "mission": rank["mission"], + } + ) + + best_data.append( + { + "course_id": course_id, + "ranking_data": ranking_data, + } + ) + + return { + "status_code": "0", + "national_best_data": best_data, + "shop_best_data": best_data, + "rank_management_flag": 0, + } + + def handle_login_checklock_request(self, data: Dict, headers: Dict): + access_code = data["accesscode"] + user_id = data["id"] + + # check if an IDAC profile already exists + p = self.data.profile.get_profile(user_id, self.version) + is_new_player = 1 if p is None else 0 + + # other: in use + return { + "status_code": "0", + # 0 = already in use, 1 = good, 2 = too new + "lock_result": 1, + "lock_date": int(datetime.now().timestamp()), + "daily_play": 1, + "session": f"{user_id}", + "shared_security_key": "a", + "session_procseq": "a", + "new_player": is_new_player, + "server_status": 1, + } + + def handle_login_unlock_request(self, data: Dict, headers: Dict): + return { + "status_code": "0", + "lock_result": 1, + } + + def handle_login_relock_request(self, data: Dict, headers: Dict): + return { + "status_code": "0", + "lock_result": 1, + "lock_date": int(datetime.now().timestamp()), + } + + def handle_login_guestplay_request(self, data: Dict, headers: Dict): + # TODO + pass + + def _generate_story_data(self, user_id: int) -> Dict: + stories = self.data.item.get_stories(user_id) + + story_data = [] + for s in stories: + chapter_id = s["chapter"] + episodes = self.data.item.get_story_episodes(user_id, chapter_id) + + episode_data = [] + for e in episodes: + episode_id = e["episode"] + difficulties = self.data.item.get_story_episode_difficulties( + user_id, episode_id + ) + + difficulty_data = [] + for d in difficulties: + difficulty_data.append( + { + "difficulty": d["difficulty"], + "play_count": d["play_count"], + "clear_count": d["clear_count"], + "play_status": d["play_status"], + "play_score": d["play_score"], + } + ) + + episode_data.append( + { + "episode": e["episode"], + "play_status": e["play_status"], + "difficulty_data": difficulty_data, + } + ) + + story_data.append( + { + "story_type": s["story_type"], + "chapter": s["chapter"], + "loop_count": s["loop_count"], + "episode_data": episode_data, + } + ) + + return story_data + + def _generate_special_data(self, user_id: int) -> Dict: + # 4 = special mode + specials = self.data.item.get_best_challenges_by_vs_type(user_id, story_type=4) + + special_data = [] + for s in specials: + special_data.append( + { + "story_type": s["story_type"], + "vs_type": s["vs_type"], + "max_clear_lv": s["max_clear_lv"], + "last_play_lv": s["last_play_lv"], + # change to last_play_course_id? + "last_play_course_id": s["course_id"], + } + ) + + return special_data + + def _generate_challenge_data(self, user_id: int) -> Dict: + # challenge mode (Bunta challenge only right now) + challenges = self.data.item.get_best_challenges_by_vs_type( + user_id, story_type=3 + ) + + challenge_data = [] + for c in challenges: + challenge_data.append( + { + "story_type": c["story_type"], + "vs_type": c["vs_type"], + "max_clear_lv": c["max_clear_lv"], + "last_play_lv": c["last_play_lv"], + # change to last_play_course_id? + "last_play_course_id": c["course_id"], + "play_count": c["play_count"], + } + ) + + return challenge_data + + def _save_stock_data(self, user_id: int, stock_data: Dict): + updated_stock_data = {} + for k, v in stock_data.items(): + if v != "": + updated_stock_data[k] = v + + if updated_stock_data: + self.data.profile.put_profile_stock( + user_id, self.version, updated_stock_data + ) + + def handle_user_getdata_request(self, data: Dict, headers: Dict): + user_id = int(headers["session"]) + + # get the user's profile, can never be None + p = self.data.profile.get_profile(user_id, self.version) + user_data = p._asdict() + del user_data["id"] + del user_data["user"] + del user_data["version"] + user_data["id"] = user_id + user_data["store_name"] = self.core_cfg.server.name + user_data["last_play_date"] = int(user_data["last_play_date"].timestamp()) + user_data["create_date"] = int(user_data["create_date"].timestamp()) + + # get the user's rank + r = self.data.profile.get_profile_rank(user_id, self.version) + rank_data = r._asdict() + del rank_data["id"] + del rank_data["user"] + del rank_data["version"] + + # add the mode_rank_data to the user_data + user_data["mode_rank_data"] = rank_data + + # get the user's avatar + a = self.data.profile.get_profile_avatar(user_id) + avatar_data = a._asdict() + del avatar_data["id"] + del avatar_data["user"] + + # get the user's stock + s = self.data.profile.get_profile_stock(user_id, self.version) + stock_data = s._asdict() + del stock_data["id"] + del stock_data["user"] + del stock_data["version"] + + # get the user's config + c = self.data.profile.get_profile_config(user_id) + config_data = c._asdict() + del config_data["id"] + del config_data["user"] + config_data["id"] = config_data.pop("config_id") + + # get the user's ticket + tickets: list = self.data.item.get_tickets(user_id) + + """ + ticket_id: + 3 = Car Dressup Points + 5 = Avatar Dressup Points + 25 = Full Tune Tickets + 34 = Full Tune Fragments + """ + + ticket_data = [] + for ticket in tickets: + ticket_data.append( + { + "ticket_id": ticket["ticket_id"], + "ticket_cnt": ticket["ticket_cnt"], + } + ) + + # get the user's course, required for the "course proeficiency" + courses = self.data.item.get_courses(user_id) + course_data = [] + for course in courses: + course_data.append( + { + "id": 0, # no clue, always 0? + "course_id": course["course_id"], + "run_counts": course["run_counts"], + # "course proeficiency" in exp points + "skill_level_exp": course["skill_level_exp"], + } + ) + + # get the profile theory data + theory_data = {} + theory = self.data.profile.get_profile_theory(user_id, self.version) + if theory is not None: + theory_data = theory._asdict() + del theory_data["id"] + del theory_data["user"] + del theory_data["version"] + + # get the users theory course data + theory_course_data = [] + theory_courses = self.data.item.get_theory_courses(user_id) + for course in theory_courses: + tmp = course._asdict() + del tmp["id"] + del tmp["user"] + tmp["update_dt"] = int(tmp["update_dt"].timestamp()) + + theory_course_data.append(tmp) + + # get the users theory partner data + theory_partner_data = [] + theory_partners = self.data.item.get_theory_partners(user_id) + for partner in theory_partners: + tmp = partner._asdict() + del tmp["id"] + del tmp["user"] + + theory_partner_data.append(tmp) + + # get the users theory running pram data + theory_running_pram_data = [] + theory_running = self.data.item.get_theory_running(user_id) + for running in theory_running: + tmp = running._asdict() + del tmp["id"] + del tmp["user"] + + theory_running_pram_data.append(tmp) + + # get the users vs info data + vs_info_data = [] + vs_info = self.data.item.get_vs_infos(user_id) + for vs in vs_info: + vs_info_data.append( + { + "battle_mode": 1, + "vs_cnt": 1, + "vs_win": vs["win_flg"], + "invalid": 0, + "str": 0, + "str_now": 0, + "lose_now": 0, + "vs_history": vs["vs_history"], + "course_select_priority": 0, + "vsinfo_course_data": [ + { + "course_id": vs["course_id"], + "vs_cnt": 1, + "vs_win": vs["win_flg"], + } + ], + } + ) + + # get the user's car + cars = self.data.item.get_cars(self.version, user_id, only_pickup=True) + fulltune_count = 0 + total_car_parts_count = 0 + car_data = [] + for car in cars: + tmp = car._asdict() + del tmp["id"] + del tmp["user"] + del tmp["version"] + + car_data.append(tmp) + # tune_level of 16 means fully tuned, so add 1 to fulltune_count + if car["tune_level"] >= 16: + fulltune_count += 1 + + # add the number of car parts to total_car_parts_count? + # total_car_parts_count += tmp["total_car_parts_count"] + + car_data.append(tmp) + + # update user profile car count + user_data["have_car_cnt"] = len(car_data) + + # get the user's play stamps + stamps = self.data.item.get_stamps(user_id) + stamp_event_data = [] + for stamp in stamps: + tmp = stamp._asdict() + del tmp["id"] + del tmp["user"] + + now = datetime.now() + + # create timestamp for today at 1am + this_day = now.replace(hour=1, minute=0, second=0, microsecond=0) + + # check if this_day is greater than or equal to create_date_daily + if this_day >= tmp["create_date_daily"]: + # reset the daily stamp + tmp["create_date_daily"] = now + tmp["daily_bonus"] = 0 + + # create a timestamp for this monday at 1am + this_monday = now - timedelta(days=now.weekday()) + this_monday = this_monday.replace(hour=1, minute=0, second=0, microsecond=0) + + # check if this_monday is greater than or equal to create_date_weekly + if this_monday >= tmp["create_date_weekly"]: + # reset the weekly stamp + tmp["create_date_weekly"] = now + tmp["weekly_bonus"] = 0 + + # update the play stamp in the database + self.data.item.put_stamp(user_id, tmp) + + del tmp["create_date_daily"] + del tmp["create_date_weekly"] + stamp_event_data.append(tmp) + + # get the user's timetrial event data + timetrial_event_data = {} + timetrial = self.data.item.get_timetrial_event(user_id, self.timetrial_event_id) + if timetrial is not None: + timetrial_event_data = { + "timetrial_event_id": timetrial["timetrial_event_id"], + "point": timetrial["point"], + } + + return { + "status_code": "0", + "user_base_data": user_data, + "avatar_data": avatar_data, + "pick_up_car_data": car_data, + "story_data": self._generate_story_data(user_id), + "vsinfo_data": vs_info_data, + "stock_data": stock_data, + "mission_data": { + "id": 0, + "achieve_flag": 0, + "received_flag": 0, + "update_dt": int(datetime.now().timestamp() - 86400), + }, + "weekly_mission_data": [], + "course_data": course_data, + "toppatu_event_data": { + "id": 0, + "event_id": 0, + "count1": 0, + "count2": 0, + "count3": 0, + "accept_flag": 0, + }, + "event_data": { + "id": 0, + "active_event_id": 0, + "dialog_show_date": int(datetime.now().timestamp() - 86400), + "show_start_dialog_flag": 1, + "show_progress_dialog_flag": 1, + "show_end_dialog_flag": 1, + "end_event_id": 0, + }, + "rewards_data": {}, + "login_bonus_data": { + "gacha_id": 0, + "gacha_item_id": 0, + "category": 0, + "type": 0, + }, + "frozen_data": {"frozen_status": 2}, + "penalty_data": {"penalty_flag": 0, "penalty_2_level": 0}, + "config_data": config_data, + "battle_gift_data": [], + "ticket_data": ticket_data, + "round_event": [], + "last_round_event": [], + "past_round_event": [], + "total_round_point": 0, + "stamp_event_data": stamp_event_data, + "avatar_gacha_lottery_data": {"avatar_gacha_id": 0}, + "fulltune_count": fulltune_count, + "total_car_parts_count": total_car_parts_count, + "car_layout_count": [], + "car_style_count": [], + "car_use_count": [], + "maker_use_count": [], + "story_course": [{"course_id": 0, "count": 1}], + # TODO! + # "driver_debut": { + # "play_count": 137, + # "daily_play": 5, + # "last_play_dt": 0, + # "use_start_date": 0, + # "use_end_date": 0, + # "use_dt": 0, + # "ticket_cnt": 0, + # "ticket_get_bit": 0, + # }, + "theory_data": theory_data, + "theory_course_data": theory_course_data, + "theory_partner_data": theory_partner_data, + "theory_running_pram_data": theory_running_pram_data, + "special_mode_data": self._generate_special_data(user_id), + "challenge_mode_data": self._generate_challenge_data(user_id), + "season_rewards_data": [], + "timetrial_event_data": timetrial_event_data, + "special_mode_hint_data": {"story_type": 0, "hint_display_flag": 0}, + } + + def handle_timetrial_getbestrecordpreta_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + for car_id in data["car_ids"]: + pass + + course_mybest_data = [] + courses = self.data.item.get_time_trial_user_best_courses(self.version, user_id) + for course in courses: + course_mybest_data.append( + { + "course_id": course["course_id"], + # local rank, store rank, worldwide rank? + "rank": 1, + # no clue + "member": 10000, + # goal_time in ms + "value": course["goal_time"], + # total number of entries per course? + "total": 10, + "store": self.core_cfg.server.name, + # use car_id from request? + "car_id": 0, + "style_car_id": course["style_car_id"], + "play_dt": course["play_dt"].timestamp(), + "section_time_1": course["section_time_1"], + "section_time_2": course["section_time_2"], + "section_time_3": course["section_time_3"], + "section_time_4": course["section_time_4"], + # no clue + "mission": course["mission"], + } + ) + + course_pickup_car_best_data = [] + courses = self.data.item.get_time_trial_courses(self.version) + for course in courses: + car_list = [] + best_cars = self.data.item.get_time_trial_best_cars_by_course( + self.version, user_id, course["course_id"] + ) + + for car in best_cars: + car_list.append( + { + "rank": 1, + # no clue + "member": user_id, + "value": car["goal_time"], + "store": self.core_cfg.server.name, + # use car_id from request? + "car_id": 0, + "style_car_id": car["style_car_id"], + "play_dt": car["play_dt"].timestamp(), + "section_time_1": car["section_time_1"], + "section_time_2": car["section_time_2"], + "section_time_3": car["section_time_3"], + "section_time_4": car["section_time_4"], + "mission": car["mission"], + } + ) + + course_pickup_car_best_data.append( + { + "course_id": course["course_id"], + "car_list": car_list, + } + ) + + return { + "status_code": "0", + "course_mybest_data": course_mybest_data, + # "course_car_best_data": course_car_best_data, + # "course_best_data": course_best_data, + "location_course_store_best_data": [], + "course_pickup_car_best_data": course_pickup_car_best_data, + } + + def handle_timetrial_getbestrecordprerace_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + for car_id in data["car_ids"]: + pass + + course_best_data = [ + { + "course_id": 56, + "rank": 1, + "member": user_id, + "value": 118016, + "store": self.core_cfg.server.name, + "car_id": 0, + "style_car_id": 0, + "play_dt": int(datetime.now().timestamp() - 86400), + "section_time_1": 45205, + "section_time_2": 47779, + "section_time_3": 43480, + "section_time_4": 41552, + "mission": 1, + } + ] + + return { + "status_code": "0", + # "course_car_best_data": course_car_best_data, + "course_best_data": course_best_data, + } + + def handle_user_createaccount_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + car_data: Dict = data.pop("car_obj") + parts_data: List = car_data.pop("parts_list") + avatar_data: Dict = data.pop("avatar_obj") + config_data: Dict = data.pop("config_obj") + + rank_data: Dict = data.pop("mode_rank_data") + stock_data: Dict = data.pop("takeover_stock_obj") + takeover_ticket_list: List = data.pop("takeover_ticket") + + # not required? + use_ticket = data.pop("use_ticket") + + # save profile in database + data["store"] = headers.get("a_store", 0) + data["country"] = headers.get("a_country", 0) + data["asset_version"] = headers.get("asset_version", 1) + self.data.profile.put_profile(user_id, self.version, data) + + # save rank data in database + self.data.profile.put_profile_rank(user_id, self.version, rank_data) + + # save stock data in database + self._save_stock_data(user_id, stock_data) + + # save tickets in database + for ticket in takeover_ticket_list: + self.data.item.put_ticket(user_id, ticket) + + config_data["config_id"] = config_data.pop("id") + self.data.profile.put_profile_config(user_id, config_data) + self.data.profile.put_profile_avatar(user_id, avatar_data) + + # save car data and car parts in database + car_data["parts_list"] = parts_data + self.data.item.put_car(user_id, self.version, car_data) + + return {"status_code": "0"} + + def handle_user_updatelogin_request(self, data: Dict, headers: Dict): + pass + + def handle_timetrial_getcarbest_request(self, data: Dict, headers: Dict): + pass + + def handle_factory_avatargacharesult_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + stock_data: Dict = data.pop("stock_obj") + use_ticket_cnt = data["use_ticket_cnt"] + + # save stock data in database + self._save_stock_data(user_id, stock_data) + + # get the user's ticket + tickets: list = self.data.item.get_tickets(user_id) + ticket_list = [] + for ticket in tickets: + # avatar tickets + if ticket["ticket_id"] == 5: + ticket_data = { + "ticket_id": ticket["ticket_id"], + "ticket_cnt": ticket["ticket_cnt"] - use_ticket_cnt, + } + + # update the ticket in the database + self.data.item.put_ticket(user_id, ticket_data) + ticket_list.append(ticket_data) + + continue + + ticket_list.append( + { + "ticket_id": ticket["ticket_id"], + "ticket_cnt": ticket["ticket_cnt"], + } + ) + + return {"status_code": "0", "ticket_data": ticket_list} + + def handle_factory_savefavoritecar_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + # save favorite cars in database + for car in data["pickup_on_car_ids"]: + self.data.item.put_car(user_id, self.version, car) + + for car in data["pickup_off_car_ids"]: + self.data.item.put_car( + user_id, + self.version, + {"style_car_id": car["style_car_id"], "pickup_seq": 0}, + ) + + return {"status_code": "0"} + + def handle_factory_updatemultiplecustomizeresult_request( + self, data: Dict, headers: Dict + ): + user_id = headers["session"] + + car_list = data.pop("car_list") + ticket_data: List = data.pop("ticket_data") + + # unused + total_car_parts_count = data.pop("total_car_parts_count") + + # save tickets in database + for ticket in ticket_data: + self.data.item.put_ticket(user_id, ticket) + + for car in car_list: + # save car data and car parts in database + self.data.item.put_car(user_id, self.version, car) + + return {"status_code": "0"} + + def handle_factory_updatecustomizeresult_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + parts_data: List = data.pop("parts_list") + ticket_data: List = data.pop("ticket_data") + + # save tickets in database + for ticket in ticket_data: + self.data.item.put_ticket(user_id, ticket) + + # save car data in database + data["parts_list"] = parts_data + self.data.item.put_car(user_id, self.version, data) + + return {"status_code": "0"} + + def handle_factory_getcardata_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + cars = self.data.item.get_cars(self.version, user_id) + car_data = [] + for car in cars: + tmp = car._asdict() + del tmp["id"] + del tmp["user"] + del tmp["version"] + + car_data.append(tmp) + + return { + "status_code": "0", + "car_data": car_data, + } + + def handle_factory_renamebefore_request(self, data: Dict, headers: Dict): + pass + + def handle_factory_buycarresult_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + parts_data: List = data.pop("parts_list") + pickup_on_list: List = data.pop("pickup_on_car_ids") + pickup_off_list: List = data.pop("pickup_off_car_ids") + + style_car_id = data.get("style_car_id") + + # get the pickup_seq for the new car + pickup_seq = 0 + # save favorite cars in database + for car in pickup_on_list: + # if the new car is a favorite get the new pickup_seqn for later + if car["style_car_id"] == style_car_id: + pickup_seq = car["pickup_seq"] + else: + self.data.item.put_car(user_id, self.version, car) + + data["pickup_seq"] = pickup_seq + + cash = data.pop("cash") + total_cash = data.pop("total_cash") + + # save the new cash in database + self.data.profile.put_profile( + user_id, self.version, {"total_cash": total_cash, "cash": cash} + ) + + # full tune ticket + use_ticket = data.pop("use_ticket") + if use_ticket: + # get the user's tickets, full tune ticket id is 25 + ticket = self.data.item.get_ticket(user_id, ticket_id=25) + + # update the ticket in the database + self.data.item.put_ticket( + user_id, + { + "ticket_id": ticket["ticket_id"], + "ticket_cnt": ticket["ticket_cnt"] - 1, + }, + ) + + # also set the tune_level to 16 (fully tuned) + data["tune_level"] = 16 + + # save car data and car parts in database + data["parts_list"] = parts_data + self.data.item.put_car(user_id, self.version, data) + + for car in pickup_off_list: + self.data.item.put_car( + user_id, + self.version, + {"style_car_id": car["style_car_id"], "pickup_seq": 0}, + ) + + # get the user's car + cars = self.data.item.get_cars(self.version, user_id) + fulltune_count = 0 + total_car_parts_count = 0 + for car in cars: + # tune_level of 16 means fully tuned, so add 1 to fulltune_count + if car["tune_level"] >= 16: + fulltune_count += 1 + + # add the number of car parts to total_car_parts_count + # total_car_parts_count += car["total_car_parts_count"] + + # get the user's ticket + tickets = self.data.item.get_tickets(user_id) + ticket_data = [] + for ticket in tickets: + ticket_data.append( + { + "ticket_id": ticket["ticket_id"], + "ticket_cnt": ticket["ticket_cnt"], + } + ) + + return { + "status_code": "0", + "ticket_data": ticket_data, + "fulltune_count": fulltune_count, + "total_car_parts_count": total_car_parts_count, + "car_layout_count": [], + "car_style_count": [], + } + + def handle_factory_renameresult_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + new_username = data.get("username") + + # save new username in database + if new_username: + self.data.profile.put_profile(user_id, self.version, data) + + return {"status_code": "0"} + + def handle_factory_updatecustomizeavatar_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + avatar_data: Dict = data.pop("avatar_obj") + stock_data: Dict = data.pop("stock_obj") + + # update the stock data in database + self._save_stock_data(user_id, stock_data) + + # save avatar data and avatar parts in database + self.data.profile.put_profile_avatar(user_id, avatar_data) + + return {"status_code": "0"} + + def handle_factory_updatecustomizeuser_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + stock_data: Dict = data.pop("stock_obj") + + # update the stock data in database + self._save_stock_data(user_id, stock_data) + + # update profile data and config in database + self.data.profile.put_profile(user_id, self.version, data) + + return {"status_code": "0"} + + def handle_user_updatestampinfo_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + stamp_event_data = data.pop("stamp_event_data") + for stamp in stamp_event_data: + self.data.item.put_stamp(user_id, stamp) + + return {"status_code": "0"} + + def handle_user_updatetimetrialresult_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + stock_data: Dict = data.pop("stock_obj") + ticket_data: List = data.pop("ticket_data") + reward_dist_data: Dict = data.pop("reward_dist_obj") + driver_debut_data = data.pop("driver_debut_obj") + rank_data: Dict = data.pop("mode_rank_obj") + + # time trial event points + event_point = data.pop("event_point") + + # save stock data in database + self._save_stock_data(user_id, stock_data) + + # save tickets in database + for ticket in ticket_data: + self.data.item.put_ticket(user_id, ticket) + + # save mode rank data in database + rank_data.update(reward_dist_data) + self.data.profile.put_profile_rank(user_id, self.version, rank_data) + + # get the profile data, update total_play and daily_play, and save it + profile = self.data.profile.get_profile(user_id, self.version) + total_play = profile["total_play"] + 1 + + # update profile + self.data.profile.put_profile( + user_id, + self.version, + { + "total_play": total_play, + "last_play_date": datetime.now(), + "aura_id": data.pop("aura_id"), + "aura_color_id": data.pop("aura_color_id"), + "aura_line_id": data.pop("aura_line_id"), + "cash": data.pop("cash"), + "total_cash": data.pop("total_cash"), + "dressup_point": data.pop("dressup_point"), + "avatar_point": data.pop("avatar_point"), + "mileage": data.pop("mileage"), + }, + ) + + # get the use_count and story_use_count of the used car + style_car_id = data.get("style_car_id") + car_mileage = data.pop("car_mileage") + used_car = self.data.item.get_car(user_id, self.version, style_car_id)._asdict() + + # increase the use_count and story_use_count of the used car + used_car["use_count"] += 1 + used_car["timetrial_use_count"] += 1 + used_car["car_mileage"] = car_mileage + + # save the used car in database + self.data.item.put_car(user_id, self.version, used_car) + + # skill_level_exp is the "course proeficiency" and is saved + # in the course table + course_id = data.get("course_id") + run_counts = 1 + skill_level_exp = data.pop("skill_level_exp") + + # get the course data + course = self.data.item.get_course(user_id, course_id) + if course: + # update run_counts + run_counts = course["run_counts"] + 1 + + self.data.item.put_course( + user_id, + { + "course_id": course_id, + "run_counts": run_counts, + "skill_level_exp": skill_level_exp, + }, + ) + + goal_time = data.get("goal_time") + # grab the ranking data and count the numbers of rows with a faster time + # than the current goal_time + course_rank = self.data.item.get_time_trial_ranking_by_course( + self.version, course_id, limit=None + ) + course_rank = len([r for r in course_rank if r["goal_time"] < goal_time]) + 1 + + car_course_rank = self.data.item.get_time_trial_ranking_by_course( + self.version, course_id, style_car_id, limit=None + ) + car_course_rank = ( + len([r for r in car_course_rank if r["goal_time"] < goal_time]) + 1 + ) + + # only update the time if its better than the best time and also not 0 + if data.get("goal_time") > 0: + # get the current best goal time + best_time_trial = ( + self.data.item.get_time_trial_user_best_time_by_course_car( + self.version, user_id, course_id, style_car_id + ) + ) + + if ( + best_time_trial is None + or data.get("goal_time") < best_time_trial["goal_time"] + ): + # now finally save the time trial with updated timestamp + data["play_dt"] = datetime.now() + self.data.item.put_time_trial(self.version, user_id, data) + + # update the timetrial event points + self.data.item.put_timetrial_event(user_id, self.timetrial_event_id, event_point) + + return { + "status_code": "0", + "course_rank": course_rank, + "course_car_rank": car_course_rank, + "location_course_store_rank": course_rank, + "car_use_count": [], + "maker_use_count": [], + } + + def handle_user_updatestoryresult_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + stock_data: Dict = data.pop("stock_obj") + ticket_data: List = data.pop("ticket_data") + reward_dist_data: Dict = data.pop("reward_dist_obj") + driver_debut_data = data.pop("driver_debut_obj") + rank_data: Dict = data.pop("mode_rank_obj") + # stamp_event_data = data.pop("stamp_event_data") + + # save stock data in database + self._save_stock_data(user_id, stock_data) + + # save tickets in database + for ticket in ticket_data: + self.data.item.put_ticket(user_id, ticket) + + # save mode rank data in database + rank_data.update(reward_dist_data) + self.data.profile.put_profile_rank(user_id, self.version, rank_data) + + # save the current story progress in database + max_loop = data.get("chapter_loop_max") + chapter_id = data.get("chapter") + + episode_id = data.get("episode") + difficulty = data.get("difficulty") + play_status = data.get("play_status") + + # get the current loop from the database + story_data = self.data.item.get_story(user_id, chapter_id) + # 1 = active, 2+ = cleared? + loop_count = 1 + if story_data: + loop_count = story_data["loop_count"] + + # if the played difficulty is smaller than loop_count you cannot clear + # (play_status = 2) the episode otherwise the following difficulties + # won't earn any EXP? + if difficulty < loop_count: + play_status = 1 + + # if the episode has already been cleared, set the play_status to 2 + # so it won't be set to unplayed (play_status = 1) + episode_data = self.data.item.get_story_episode(user_id, episode_id) + if episode_data: + if play_status < episode_data["play_status"]: + play_status = 2 + + # save the current episode progress in database + self.data.item.put_story_episode( + user_id, + chapter_id, + { + "episode": episode_id, + "play_status": play_status, + }, + ) + + if loop_count < max_loop and data.get("chapter_clear") == 1: + # increase the loop count + loop_count += 1 + + # for the current chapter set all episode play_status back to 1 + self.data.item.put_story_episode_play_status(user_id, chapter_id, 1) + + self.data.item.put_story( + user_id, + { + "story_type": data.get("story_type"), + "chapter": chapter_id, + "loop_count": loop_count, + }, + ) + + # save the current episode difficulty progress in database + self.data.item.put_story_episode_difficulty( + user_id, + episode_id, + { + "difficulty": difficulty, + "play_count": 1, # no idea where this comes from + "clear_count": 1, # no idea where this comes from + "play_status": data.get("play_status"), + "play_score": data.get("play_score"), + }, + ) + + # get the use_count and story_use_count of the used car + style_car_id = data.get("style_car_id") + car_mileage = data.get("car_mileage") + used_car = self.data.item.get_car(user_id, self.version, style_car_id)._asdict() + + # increase the use_count and story_use_count of the used car + used_car["use_count"] += 1 + used_car["story_use_count"] += 1 + used_car["car_mileage"] = car_mileage + + # save the used car in database + self.data.item.put_car(user_id, self.version, used_car) + + # get the profile data, update total_play and daily_play, and save it + profile = self.data.profile.get_profile(user_id, self.version) + total_play = profile["total_play"] + 1 + + # save user profile in database + self.data.profile.put_profile( + user_id, + self.version, + { + "total_play": total_play, + "last_play_date": datetime.now(), + "mileage": data.pop("mileage"), + "cash": data.pop("cash"), + "total_cash": data.pop("total_cash"), + "dressup_point": data.pop("dressup_point"), + "avatar_point": data.pop("avatar_point"), + "aura_id": data.pop("aura_id"), + "aura_color_id": data.pop("aura_color_id"), + "aura_line_id": data.pop("aura_line_id"), + }, + ) + + return { + "status_code": "0", + "story_data": self._generate_story_data(user_id), + "car_use_count": [], + "maker_use_count": [], + } + + def handle_user_updatespecialmoderesult_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + stock_data: Dict = data.pop("stock_obj") + ticket_data: List = data.pop("ticket_data") + reward_dist_data: Dict = data.pop("reward_dist_obj") + driver_debut_data = data.pop("driver_debut_obj") + rank_data: Dict = data.pop("mode_rank_obj") + + # unused + hint_display_flag: int = data.pop("hint_display_flag") + + # get the vs use count from database and update it + style_car_id = data.pop("style_car_id") + car_data = self.data.item.get_car(user_id, self.version, style_car_id) + vs_use_count = car_data["vs_use_count"] + 1 + + # save car data in database + self.data.item.put_car( + user_id, + self.version, + { + "style_car_id": style_car_id, + "car_mileage": data.pop("car_mileage"), + "vs_use_count": vs_use_count, + }, + ) + + # get the profile data, update total_play and daily_play, and save it + profile = self.data.profile.get_profile(user_id, self.version) + total_play = profile["total_play"] + 1 + + # save user profile in database + self.data.profile.put_profile( + user_id, + self.version, + { + "total_play": total_play, + "last_play_date": datetime.now(), + "mileage": data.pop("mileage"), + "cash": data.pop("cash"), + "total_cash": data.pop("total_cash"), + "dressup_point": data.pop("dressup_point"), + "avatar_point": data.pop("avatar_point"), + "aura_id": data.pop("aura_id"), + "aura_color_id": data.pop("aura_color_id"), + "aura_line_id": data.pop("aura_line_id"), + }, + ) + + # save stock data in database + self._save_stock_data(user_id, stock_data) + + # save ticket data in database + for ticket in ticket_data: + self.data.item.put_ticket(user_id, ticket) + + # save mode_rank and reward_dist data in database + rank_data.update(reward_dist_data) + self.data.profile.put_profile_rank(user_id, self.version, rank_data) + + # finally save the special mode with story_type=4 in database + self.data.item.put_challenge(user_id, data) + + return { + "status_code": "0", + "special_mode_data": self._generate_special_data(user_id), + "car_use_count": [], + "maker_use_count": [], + } + + def handle_user_updatechallengemoderesult_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + stock_data: Dict = data.pop("stock_obj") + ticket_data: List = data.pop("ticket_data") + reward_dist_data: Dict = data.pop("reward_dist_obj") + driver_debut_data = data.pop("driver_debut_obj") + rank_data: Dict = data.pop("mode_rank_obj") + + # get the vs use count from database and update it + style_car_id = data.get("style_car_id") + car_data = self.data.item.get_car(user_id, self.version, style_car_id) + vs_use_count = car_data["vs_use_count"] + 1 + + # save car data in database + self.data.item.put_car( + user_id, + self.version, + { + "style_car_id": style_car_id, + "car_mileage": data.pop("car_mileage"), + "vs_use_count": vs_use_count, + }, + ) + + # get the profile data, update total_play and daily_play, and save it + profile = self.data.profile.get_profile(user_id, self.version) + total_play = profile["total_play"] + 1 + + # save user profile in database + self.data.profile.put_profile( + user_id, + self.version, + { + "total_play": total_play, + "last_play_date": datetime.now(), + "mileage": data.pop("mileage"), + "cash": data.pop("cash"), + "total_cash": data.pop("total_cash"), + "dressup_point": data.pop("dressup_point"), + "avatar_point": data.pop("avatar_point"), + "aura_id": data.pop("aura_id"), + "aura_color_id": data.pop("aura_color_id"), + "aura_line_id": data.pop("aura_line_id"), + }, + ) + + # save stock data in database + self._save_stock_data(user_id, stock_data) + + # save ticket data in database + for ticket in ticket_data: + self.data.item.put_ticket(user_id, ticket) + + # save mode_rank and reward_dist data in database + rank_data.update(reward_dist_data) + self.data.profile.put_profile_rank(user_id, self.version, rank_data) + + # get the challenge mode data from database + challenge_data = self.data.item.get_challenge( + user_id, data.get("vs_type"), data.get("play_difficulty") + ) + + if challenge_data: + # update play count + play_count = challenge_data["play_count"] + 1 + data["play_count"] = play_count + + # finally save the challenge mode with story_type=3 in database + self.data.item.put_challenge(user_id, data) + + return { + "status_code": "0", + "challenge_mode_data": self._generate_challenge_data(user_id), + "car_use_count": [], + "maker_use_count": [], + } + + def handle_user_updatecartune_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + # full tune ticket + use_ticket = data.pop("use_ticket") + if use_ticket: + # get the user's tickets, full tune ticket id is 25 + ticket = self.data.item.get_ticket(user_id, ticket_id=25) + + # update the ticket in the database + self.data.item.put_ticket( + user_id, + { + "ticket_id": ticket["ticket_id"], + "ticket_cnt": ticket["ticket_cnt"] - 1, + }, + ) + + # also set the tune_level to 16 (fully tuned) + data["tune_level"] = 16 + + self.data.item.put_car(user_id, self.version, data) + + return { + "status_code": "0", + "story_data": self._generate_story_data(user_id), + "car_use_count": [], + "maker_use_count": [], + } + + def handle_log_saveplaylog_request(self, data: Dict, headers: Dict): + pass + + def handle_log_saveendlog_request(self, data: Dict, headers: Dict): + pass + + def handle_user_updatemoderesult_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + config_data: Dict = data.pop("config_obj") + stock_data: Dict = data.pop("stock_obj") + ticket_data: List = data.pop("ticket_data") + reward_dist_data: Dict = data.pop("reward_dist_obj") + + # not required? + mode_id = data.pop("mode_id") + standby_play_flag = data.pop("standby_play_flag") + tips_list = data.pop("tips_list") + + # save stock data in database + self._save_stock_data(user_id, stock_data) + + # save tickets in database + for ticket in ticket_data: + self.data.item.put_ticket(user_id, ticket) + + # save rank dist data in database + self.data.profile.put_profile_rank(user_id, self.version, reward_dist_data) + + # update profile data and config in database + self.data.profile.put_profile(user_id, self.version, data) + config_data["config_id"] = config_data.pop("id") + self.data.profile.put_profile_config(user_id, config_data) + + return {"status_code": "0", "server_status": 1} + + def _generate_theory_rival_data( + self, user_list: list, course_id: int, req_user_id: int + ) -> list: + rival_data = [] + for user_id in user_list: + # if not enough players are available just use the data from the req_user + if user_id == -1: + profile = self.data.profile.get_profile(req_user_id, self.version) + profile = profile._asdict() + # set the name to CPU + profile["username"] = f"CPU" + # also reset stamps to default + profile["stamp_key_assign_0"] = 0 + profile["stamp_key_assign_1"] = 1 + profile["stamp_key_assign_2"] = 2 + profile["stamp_key_assign_3"] = 3 + profile["mytitle_id"] = 0 + else: + profile = self.data.profile.get_profile(user_id, self.version) + + rank = self.data.profile.get_profile_rank(profile["user"], self.version) + + avatars = [ + { + "sex": 0, + "face": 1, + "eye": 1, + "mouth": 1, + "hair": 1, + "glasses": 0, + "face_accessory": 0, + "body": 1, + "body_accessory": 0, + "behind": 0, + "bg": 1, + "effect": 0, + "special": 0, + }, + { + "sex": 0, + "face": 1, + "eye": 1, + "mouth": 1, + "hair": 19, + "glasses": 0, + "face_accessory": 0, + "body": 2, + "body_accessory": 0, + "behind": 0, + "bg": 1, + "effect": 0, + "special": 0, + }, + { + "sex": 1, + "face": 91, + "eye": 265, + "mouth": 13, + "hair": 369, + "glasses": 0, + "face_accessory": 0, + "body": 113, + "body_accessory": 0, + "behind": 0, + "bg": 1, + "effect": 0, + "special": 0, + }, + { + "sex": 1, + "face": 91, + "eye": 265, + "mouth": 13, + "hair": 387, + "glasses": 0, + "face_accessory": 0, + "body": 114, + "body_accessory": 0, + "behind": 0, + "bg": 1, + "effect": 0, + "special": 0, + }, + ] + + if user_id == -1: + # get a random avatar from the list and some random car from all users + avatar = choice(avatars) + car = self.data.item.get_random_car(self.version) + else: + avatar = self.data.profile.get_profile_avatar(profile["user"]) + car = self.data.item.get_random_user_car(profile["user"], self.version) + + parts_list = [] + for part in car["parts_list"]: + parts_list.append(part["parts"]) + + course = self.data.item.get_theory_course(profile["user"], course_id) + powerhose_lv = 0 + if course: + powerhose_lv = course["powerhouse_lv"] + + theory_running = self.data.item.get_theory_running_by_course( + profile["user"], course_id + ) + + # normally it's 127 after the first play so we set it to 128 + attack = 128 + defense = 128 + safety = 128 + runaway = 128 + trick_flag = 0 + if theory_running and user_id != -1: + attack = theory_running["attack"] + defense = theory_running["defense"] + safety = theory_running["safety"] + runaway = theory_running["runaway"] + trick_flag = theory_running["trick_flag"] + + # get the time trial ranking medal + eval_id = 0 + time_trial = self.data.item.get_time_trial_best_ranking_by_course( + self.version, profile["user"], course_id + ) + if time_trial: + eval_id = time_trial["eval_id"] + + rival_data.append( + { + "id": profile["user"], + "name": profile["username"], + "grade": rank["grade"], + # only needed for power match + "powerhouseLv": powerhose_lv, + "mytitleId": profile["mytitle_id"], + "country": 9, + "auraId": profile["aura_id"], + "auraColor": profile["aura_color_id"], + "auraLine": profile["aura_line_id"], + # not sure? + "roundRanking": 0, + "storeName": self.core_cfg.server.name, + "sex": avatar["sex"], + "face": avatar["face"], + "eye": avatar["eye"], + "mouth": avatar["mouth"], + "hair": avatar["hair"], + "glasses": avatar["glasses"], + "faceAccessory": avatar["face_accessory"], + "body": avatar["body"], + "bodyAccessory": avatar["body_accessory"], + "behind": avatar["behind"], + "bg": avatar["bg"], + "effect": avatar["effect"], + "special": avatar["special"], + "styleCarId": car["style_car_id"], + "color": car["color"], + "bureau": car["bureau"], + "kana": car["kana"], + "sNo": car["s_no"], + "lNo": car["l_no"], + "tuneLv": car["tune_level"], + "carFlag": car["car_flag"], + "tunePoint": car["tune_point"], + "infinityTune": car["infinity_tune"], + "tuneParts": car["tune_parts"], + "partsList": parts_list, + "partsCount": car["equip_parts_count"], + "stamp0": profile["stamp_key_assign_0"], + "stamp1": profile["stamp_key_assign_1"], + "stamp2": profile["stamp_key_assign_2"], + "stamp3": profile["stamp_key_assign_3"], + "attack": attack, + "defense": defense, + "safety": safety, + "runaway": runaway, + "trickFlg": trick_flag, + # time trial ranking medal + "taEval": eval_id, + } + ) + + return rival_data + + def handle_theory_matching_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + course_id = data.pop("course_id") + # no idea why thats needed? + grade = data.pop("grade") + + # number of auto_matches and power_matches, official values are: + count_auto_match = 9 + count_power_match = 3 + + # required for the power_match list? + powerhose_lv = data.pop("powerhouse_lv") + + # get random profiles for auto match + profiles = self.data.profile.get_different_random_profiles( + user_id, self.version, count=count_auto_match + ) + + user_list = [profile["user"] for profile in profiles] + # if user_list is not count_auto_match long, fill it up with -1 + while len(user_list) < count_auto_match: + user_list.append(-1) + + auto_match = self._generate_theory_rival_data(user_list, course_id, user_id) + + # get profiles with the same powerhouse_lv for power match + theory_courses = self.data.item.get_theory_course_by_powerhouse_lv( + user_id, course_id, powerhose_lv, count=count_power_match + ) + user_list = [course["user"] for course in theory_courses] + + # if user_list is not count_power_match long, fill it up with -1 + while len(user_list) < count_power_match: + user_list.append(-1) + + power_match = self._generate_theory_rival_data(user_list, course_id, user_id) + + return { + "status_code": "0", + "server_status": 1, + "rival_data": { + "auto_match": auto_match, + "power_match": power_match, + }, + } + + def handle_user_updatetheoryresult_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + stock_data: Dict = data.pop("stock_obj") + ticket_data: List = data.pop("ticket_data") + reward_dist_data: Dict = data.pop("reward_dist_obj") + rank_data: Dict = data.pop("mode_rank_obj") + driver_debut_data: Dict = data.pop("driver_debut_obj") + + # save stock data in database + self._save_stock_data(user_id, stock_data) + + # save tickets in database + for ticket in ticket_data: + self.data.item.put_ticket(user_id, ticket) + + # save rank dist data in database + rank_data.update(reward_dist_data) + self.data.profile.put_profile_rank(user_id, self.version, rank_data) + + # save the profile theory data in database + play_count = 1 + play_count_multi = 1 + win_count = 0 + win_count_multi = 0 + + theory_data = self.data.profile.get_profile_theory(user_id, self.version) + if theory_data: + play_count = theory_data["play_count"] + 1 + play_count_multi = theory_data["play_count_multi"] + 1 + win_count = theory_data["win_count"] + win_count_multi = theory_data["win_count_multi"] + + # check all advantages and see if one of them is larger than 0 + # if so, we won + if ( + data.get("advantage_1") > 0 + or data.get("advantage_2") > 0 + or data.get("advantage_3") > 0 + or data.get("advantage_4") > 0 + ): + win_count += 1 + win_count_multi += 1 + + self.data.profile.put_profile_theory( + user_id, + self.version, + { + "play_count": play_count, + "play_count_multi": play_count_multi, + "partner_id": data.get("partner_id"), + "partner_progress": data.get("partner_progress"), + "partner_progress_score": data.get("partner_progress_score"), + "practice_start_rank": data.get("practice_start_rank"), + "general_flag": data.get("general_flag"), + "vs_history": data.get("vs_history"), + # no idea? + "vs_history_multi": data.get("vs_history"), + "win_count": win_count, + "win_count_multi": win_count_multi, + }, + ) + + # save theory course in database + self.data.item.put_theory_course( + user_id, + { + "course_id": data.get("course_id"), + "max_victory_grade": data.get("max_victory_grade"), + # always add 1? + "run_count": 1, + "powerhouse_lv": data.get("powerhouse_lv"), + "powerhouse_exp": data.get("powerhouse_exp"), + # not sure if the played_powerhouse_lv is the same as powerhouse_lv + "played_powerhouse_lv": data.get("powerhouse_lv"), + }, + ) + + # save the theory partner in database + self.data.item.put_theory_partner( + user_id, + { + "partner_id": data.get("partner_id"), + "fellowship_lv": data.get("fellowship_lv"), + "fellowship_exp": data.get("fellowship_exp"), + }, + ) + + # save the theory running in database? + self.data.item.put_theory_running( + user_id, + { + "course_id": data.get("course_id"), + "attack": data.get("attack"), + "defense": data.get("defense"), + "safety": data.get("safety"), + "runaway": data.get("runaway"), + "trick_flag": data.get("trick_flag"), + }, + ) + + # get the use_count and theory_use_count of the used car + style_car_id = data.get("style_car_id") + car_mileage = data.get("car_mileage") + used_car = self.data.item.get_car(user_id, self.version, style_car_id)._asdict() + + # increase the use_count and theory_use_count of the used car + used_car["use_count"] += 1 + used_car["theory_use_count"] += 1 + used_car["car_mileage"] = car_mileage + + # save the used car in database + self.data.item.put_car(user_id, self.version, used_car) + + # get the profile data, update total_play and daily_play, and save it + profile = self.data.profile.get_profile(user_id, self.version) + total_play = profile["total_play"] + 1 + + # save the profile in database + self.data.profile.put_profile( + user_id, + self.version, + { + "total_play": total_play, + "last_play_date": datetime.now(), + "mileage": data.get("mileage"), + "aura_id": data.get("aura_id"), + "aura_color_id": data.get("aura_color_id"), + "aura_line_id": data.get("aura_line_id"), + "cash": data.get("cash"), + "total_cash": data.get("total_cash"), + "dressup_point": data.get("dressup_point"), + "avatar_point": data.get("avatar_point"), + }, + ) + + return { + "status_code": "0", + "played_powerhouse_lv": data.get("powerhouse_lv"), + "car_use_count": [], + "maker_use_count": [], + "play_count": play_count, + "play_count_multi": play_count_multi, + "win_count": win_count, + "win_count_multi": win_count_multi, + } + + def handle_user_updatestorebattleresult_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + stock_data: Dict = data.pop("stock_obj") + ticket_data: List = data.pop("ticket_data") + reward_dist_data: Dict = data.pop("reward_dist_obj") + rank_data: Dict = data.pop("mode_rank_obj") + driver_debut_data: Dict = data.pop("driver_debut_obj") + + # no idea? + result = data.pop("result") + battle_gift_event_id = data.pop("battle_gift_event_id") + gift_id = data.pop("gift_id") + + # save stock data in database + self._save_stock_data(user_id, stock_data) + + # save tickets in database + for ticket in ticket_data: + self.data.item.put_ticket(user_id, ticket) + + # save rank dist data in database + rank_data.update(reward_dist_data) + self.data.profile.put_profile_rank(user_id, self.version, rank_data) + + # get the use_count and net_vs_use_count of the used car + style_car_id = data.get("style_car_id") + car_mileage = data.pop("car_mileage") + used_car = self.data.item.get_car(user_id, self.version, style_car_id)._asdict() + + # increase the use_count and net_vs_use_count of the used car + used_car["use_count"] += 1 + used_car["net_vs_use_count"] += 1 + used_car["car_mileage"] = car_mileage + + # save the used car in database + self.data.item.put_car(user_id, self.version, used_car) + + # get the profile data, update total_play and daily_play, and save it + profile = self.data.profile.get_profile(user_id, self.version) + total_play = profile["total_play"] + 1 + + # save the profile in database + self.data.profile.put_profile( + user_id, + self.version, + { + "total_play": total_play, + "last_play_date": datetime.now(), + "mileage": data.pop("mileage"), + "aura_id": data.pop("aura_id"), + "aura_color_id": data.pop("aura_color_id"), + "aura_line_id": data.pop("aura_line_id"), + "cash": data.pop("cash"), + "total_cash": data.pop("total_cash"), + "dressup_point": data.pop("dressup_point"), + "avatar_point": data.pop("avatar_point"), + }, + ) + + # save vs_info in database + self.data.item.put_vs_info(user_id, data) + + vs_info = { + "battle_mode": 0, + "vs_cnt": 1, + "vs_win": data.get("win_flg"), + "invalid": 0, + "str": 0, + "str_now": 0, + "lose_now": 0, + "vs_history": data.get("vs_history"), + "course_select_priority": 0, + "vsinfo_course_data": [ + { + "course_id": data.get("course_id"), + "vs_cnt": 1, + "vs_win": data.get("win_flg"), + } + ], + } + + return { + "status_code": "0", + "vsinfo_data": vs_info, + "car_use_count": [], + "maker_use_count": [], + } From 7033234b23a268b7f70284beae018ac1149f4cf9 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Sun, 1 Oct 2023 21:41:08 +0200 Subject: [PATCH 002/101] IDAC: Added better doc, fixed timetrial event --- docs/game_specific_info.md | 42 ++++++++++++++++++ .../data/stamps/touhou_flandre_scarlet.json | Bin 10735 -> 10737 bytes .../data/stamps/touhou_remilia_scarlet.json | Bin 10669 -> 10695 bytes .../data/stamps/touhou_sakuya_izayoi.json | Bin 10622 -> 10684 bytes titles/idac/season2.py | 6 ++- 5 files changed, 47 insertions(+), 1 deletion(-) diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index 2baa12c..20d4a23 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -597,6 +597,48 @@ python dbutils.py --game SDGT upgrade 34. Full Tune 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 +- 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. diff --git a/titles/idac/data/stamps/touhou_flandre_scarlet.json b/titles/idac/data/stamps/touhou_flandre_scarlet.json index 31638e1c8ecf2d6e1c31b307b4c3193d28e203db..23d2a7d61f0b163210d92b70b806df91199addc0 100644 GIT binary patch delta 23 ecmaDK{4sdKMjj<210!QyLjzp{L#2(oO*8>(6bHlr delta 21 ccmewu{62WXMlM4$GjnrGb3-%BjeAWr0br8{xBvhE diff --git a/titles/idac/data/stamps/touhou_remilia_scarlet.json b/titles/idac/data/stamps/touhou_remilia_scarlet.json index 82eeff481d160bea35ce9a09d96d04112d6b028c..3d917a9201e693a63746cef62ec21c03ea3f7d55 100644 GIT binary patch delta 64 zcmV-G0KfmOQ^!+~sth7BFfubOF)%GKF(R?FC;4Uf9fQ^%(4^OlGC9nf-JRZ;h delta 37 tcmX>eyf%2kDlS7aGjnrGb3-%BjoUOB8O=A}XS}F9*+AHTv$)y@W&rt{43Pi; diff --git a/titles/idac/data/stamps/touhou_sakuya_izayoi.json b/titles/idac/data/stamps/touhou_sakuya_izayoi.json index 029358c702d4bdd867c74bedf4b153d033dc1543..7b98fb31486110eed551a2f2e07b631baec3516b 100644 GIT binary patch delta 35 rcmewtv?qAN3?3yT10!QyLjzp{L#2(2-l Date: Sun, 1 Oct 2023 22:17:06 +0200 Subject: [PATCH 003/101] IDAC: Fixed time trial events config, added new events --- titles/idac/config.py | 2 +- .../timetrial/touhou_flandre_scarlet.json | Bin 0 -> 1189 bytes .../timetrial/touhou_remilia_scarlet.json | Bin 1131 -> 1183 bytes .../data/timetrial/touhou_sakuya_izayoi.json | Bin 0 -> 1165 bytes 4 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 titles/idac/data/timetrial/touhou_flandre_scarlet.json create mode 100644 titles/idac/data/timetrial/touhou_sakuya_izayoi.json diff --git a/titles/idac/config.py b/titles/idac/config.py index e685e2e..2f97219 100644 --- a/titles/idac/config.py +++ b/titles/idac/config.py @@ -109,7 +109,7 @@ class IDACTimetrialConfig: self.__config, "idac", "timetrial", - "enabled_stamps", + "enabled_timetrial", default="touhou_remilia_scarlet", ) diff --git a/titles/idac/data/timetrial/touhou_flandre_scarlet.json b/titles/idac/data/timetrial/touhou_flandre_scarlet.json new file mode 100644 index 0000000000000000000000000000000000000000..314881aa584a8266de554ffc54ea2c4f79549fda GIT binary patch literal 1189 zcmb_bOK!s;6x`<&5$ifOBzBWrC8{iH)08T~7ACEtD4Vcp570wY?XIh;y$u&B_=Azs zR746ZM#Ib--hs231)O$4WSm6AuoL`rioAI4V=$?%$J2$XzP@=eK4C0~^MQSx2M zZ*}-m2ffkoLrJORGiYTSzA~zNAkzLBws Rss2P?V`fWTgN@BEm;tH-6(0Zq delta 17 ZcmbQw`I=)w=)_KKW@BB$joV)^0{}b)2CV=9 diff --git a/titles/idac/data/timetrial/touhou_sakuya_izayoi.json b/titles/idac/data/timetrial/touhou_sakuya_izayoi.json new file mode 100644 index 0000000000000000000000000000000000000000..9cf05850515d83b6ac5346c0350d2089a32bef0b GIT binary patch literal 1165 zcmb_ay-ve07~K05Rc6xICaKcC3PO=dOfhPm$oUGYstyc{EX<6oEWA_R13N#?C($4y zHACdn`OfG2e4NeA7l7Oe(2{R6@gx*vvIy4mi8Yi(nX4f)8k!PMhT%#HQ4Tj2jz9$1&nfaHvGPAx_itJDkm( yu|b{va%p*&@ie+k1UC<2!p#g;P13izoN%oK7_AI;S+bZ1V02%}zg)$N5qK literal 0 HcmV?d00001 From ee5f13a3bb7e93618217e7334ba3027e706d49c2 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Sun, 1 Oct 2023 22:29:50 +0200 Subject: [PATCH 004/101] IDAC: Added changelog, fixed doc --- changelog.md | 12 ++++++++++++ docs/game_specific_info.md | 27 ++++++++++++++------------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/changelog.md b/changelog.md index 40d2e48..e1ecbb9 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,18 @@ # Changelog Documenting updates to ARTEMiS, to be updated every time the master branch is pushed to. +## 20231001 +### 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 + ## 20230716 ### General + Docker files added (#19) diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index 20d4a23..c328ce2 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -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 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 - [Supported Games](#supported-games) @@ -62,9 +68,7 @@ The importer for Chunithm will import: Events, Music, Charge Items and Avatar Ac ### 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. `SDBT_4_upgrade.sql`. In order to upgrade to version 4 in this case you need to -perform all previous updates as well: +Always make sure your database (tables) are up-to-date: ```shell python dbutils.py --game SDBT upgrade @@ -175,11 +179,12 @@ The importer for maimai Pre-DX will import Events and Music. Not all games will ### 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 python dbutils.py --game SDEZ upgrade ``` + Pre-Dx uses the same database as DX, so only upgrade using the SDEZ game code! ## Hatsune Miku Project Diva @@ -215,9 +220,7 @@ Config file is located in `config/diva.yaml`. ### 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. `SBZV_4_upgrade.sql`. In order to upgrade to version 4 in this case you need to -perform all previous updates as well: +Always make sure your database (tables) are up-to-date: ```shell python dbutils.py --game SBZV upgrade @@ -263,9 +266,7 @@ Note: 1149 and higher are only for Card Maker 1.35 and higher and will be ignore ### 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. `SDDT_4_upgrade.sql`. In order to upgrade to version 4 in this case you need to -perform all previous updates as well: +Always make sure your database (tables) are up-to-date: ```shell python dbutils.py --game SDDT upgrade @@ -424,7 +425,7 @@ Config file is located in `config/wacca.yaml`. ### 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 python dbutils.py --game SDFE upgrade @@ -497,7 +498,7 @@ Config file is located in `config/sao.yaml`. ### 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 python dbutils.py --game SDEW upgrade @@ -558,7 +559,7 @@ Config file is located in `config/idac.yaml`. ### Database upgrade -Always make sure your database (tables) are up-to-date +Always make sure your database (tables) are up-to-date: ```shell python dbutils.py --game SDGT upgrade From 0e664e03f979e73c80e927925894ff4275ce2d2f Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Tue, 3 Oct 2023 16:50:52 +0200 Subject: [PATCH 005/101] IDAC: Updated docs, added TA Data Known issue: after selecting Season 1, the TA Data for Season 2 will not be shown --- docs/INSTALL_WINDOWS.md | 51 ++++++++++----- docs/game_specific_info.md | 54 ++++++++-------- example_config/idac.yaml | 2 +- titles/idac/season2.py | 124 ++++++++++++++++++++++++++++--------- 4 files changed, 158 insertions(+), 73 deletions(-) diff --git a/docs/INSTALL_WINDOWS.md b/docs/INSTALL_WINDOWS.md index e88def3..b976b26 100644 --- a/docs/INSTALL_WINDOWS.md +++ b/docs/INSTALL_WINDOWS.md @@ -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 ## 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) -2. Install 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.34.0.msi 1. Click on "Add ..." on the side 2. Click on the "+" next to MySQL Servers - 3. Make sure MySQL Server 8.0.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 5. Select the configuration type "Development Computer" 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 > 10. Click on Execute and for it to finish and hit Next> and then Finish 3. Open MySQL 8.0 Command Line Client and login as your root user -4. Type those commands to create your user and the database -``` -CREATE USER 'aime'@'localhost' IDENTIFIED BY 'MyStrongPass.'; +4. Change `` to a new password for the user aime, type those commands to create your user and the database + +```sql +CREATE USER 'aime'@'localhost' IDENTIFIED BY ''; CREATE DATABASE aime; GRANT Alter,Create,Delete,Drop,Index,Insert,References,Select,Update ON aime.* TO 'aime'@'localhost'; FLUSH PRIVILEGES; @@ -34,33 +35,50 @@ exit; ## Install Python modules 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 +## Copy/Rename the folder `example_config` to `config` -1. Make sure to change the server listen_address to be set to your local machine IP (ex.: 192.168.1.xxx) +## Adjust `config/core.yaml` + +1. Make sure to change the server `hostname` to be set to your local machine IP (ex.: 192.168.xxx.xxx) - In case you want to run this only locally, set the following values: -``` + +```yaml server: listen_address: 0.0.0.0 title: - hostname: localhost + hostname: 192.168.xxx.xxx +``` + +1. Adjust the proper MySQL information you created earlier +```yaml +database: + host: "localhost" + username: "aime" + password: "" + name: "aime" ``` -2. Adjust the proper MySQL information you created earlier 3. Add the AimeDB key at the bottom of the file 4. If the webui is needed, change the flag from False to True ## Create the database tables for ARTEMiS -> python dbutils.py create + +```shell +python dbutils.py create +``` ## Firewall Adjustements Make sure the following ports are open both on your router and local Windows firewall in case you want to use this for public use (NOT recommended): > Port 80 (TCP), 443 (TCP), 8443 (TCP), 22345 (TCP), 8080 (TCP), 8090 (TCP) **webui, 8444 (TCP) **mucha ## Running the ARTEMiS instance -> python index.py +```shell +python index.py +``` # 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' 1. This means the pyYAML module is obsolete, simply rerun pip with the -U (force update) flag, as shown below. - Change your work path to the artemis-master (or artemis-develop) folder using 'cd' and run the following commands: -``` + +```shell pip install -r requirements.txt -U ``` diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index c328ce2..615f10b 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -571,32 +571,34 @@ python dbutils.py --game SDGT upgrade ### Item categories -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. Full Tune Ticket -26. Paper Cup -27. BGM -28. Drifting Text -31. Start Menu BG -32. Car Color/Paint -33. Aura Level? -34. Full Tune Ticket Fragment -35. Underneon Lights +| 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: diff --git a/example_config/idac.yaml b/example_config/idac.yaml index 04ea1e0..331ae82 100644 --- a/example_config/idac.yaml +++ b/example_config/idac.yaml @@ -1,6 +1,6 @@ server: enabled: True - loglevel: "debug" + loglevel: "info" ssl: False ssl_key: "cert/idac.key" ssl_cert: "cert/idac.crt" diff --git a/titles/idac/season2.py b/titles/idac/season2.py index 3142d49..5717978 100644 --- a/titles/idac/season2.py +++ b/titles/idac/season2.py @@ -75,32 +75,32 @@ class IDACSeason2(IDACBase): def handle_boot_getconfigdata_request(self, data: Dict, headers: Dict): """ category: - 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 = Full Tune Ticket - 26 = Paper Cup - 27 = BGM - 28 = Drifting Text - 31 = Start Menu BG - 32 = Car Color/Paint - 33 = Aura Level? - 34 = Full Tune Ticket Fragment - 35 = Underneon Lights + 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 """ version = headers["device_version"] ver_str = version.replace(".", "")[:3] @@ -146,9 +146,9 @@ class IDACSeason2(IDACBase): "round_event_exp": [], "stamp_info": self.stamp_info, # 0 = use default data, 1+ = server version of timereleasedata response - "timerelease_no": 5, + "timerelease_no": 1, # 0 = use default data, 1+ = server version of gachadata response - "timerelease_avatar_gacha_no": 5, + "timerelease_avatar_gacha_no": 1, "takeover_reward": [], "subcard_judge": [ { @@ -316,7 +316,7 @@ class IDACSeason2(IDACBase): # get the country id from the profile, 9 is JPN "country": 9, "style_car_id": rank["style_car_id"], - # convert the dateimt to a timestamp + # convert the datetime to a timestamp "play_dt": int(rank["play_dt"].timestamp()), "section_time_1": rank["section_time_1"], "section_time_2": rank["section_time_2"], @@ -341,7 +341,6 @@ class IDACSeason2(IDACBase): } def handle_login_checklock_request(self, data: Dict, headers: Dict): - access_code = data["accesscode"] user_id = data["id"] # check if an IDAC profile already exists @@ -1576,6 +1575,71 @@ class IDACSeason2(IDACBase): "maker_use_count": [], } + def _generate_time_trial_data(self, season_id: int, user_id: int) -> List[Dict]: + # get the season time trial data from database + timetrial_data = [] + + courses = self.data.item.get_courses(user_id) + if courses is None or len(courses) == 0: + return {"status_code": "0", "timetrial_data": timetrial_data} + + for course in courses: + # grab the course id and course proeficiency + course_id = course["course_id"] + skill_level_exp = course["skill_level_exp"] + + # get the best time for the current course for the current user + best_trial = self.data.item.get_time_trial_best_ranking_by_course(season_id, user_id, course_id) + if not best_trial: + continue + + goal_time = best_trial["goal_time"] + # get the rank for the current course + course_rank = self.data.item.get_time_trial_ranking_by_course( + season_id, course_id, limit=None + ) + course_rank = len([r for r in course_rank if r["goal_time"] < goal_time]) + 1 + + timetrial_data.append( + { + "style_car_id": best_trial["style_car_id"], + "course_id": course_id, + "skill_level_exp": skill_level_exp, + "goal_time": goal_time, + "rank": course_rank, + "rank_dt": int(best_trial["play_dt"].timestamp()), + }) + + return timetrial_data + + def handle_user_getpastseasontadata_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + season_id = data.get("season_id") + + # so to get the season 1 data just subtract 1 from the season id + past_timetrial_data = self._generate_time_trial_data(season_id - 1, user_id) + + # TODO: get the current season timetrial data somehow, because after requesting + # GetPastSeasonTAData the game will NOT request GetTAData?! + return { + "status_code": "0", + "season_id": season_id, + "past_season_timetrial_data": past_timetrial_data, + } + + def handle_user_gettadata_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + timetrial_data = self._generate_time_trial_data(self.version, user_id) + + # TODO: get the past season timetrial data somehow, because after requesting + # GetTAData the game will NOT request GetPastSeasonTAData?! + return { + "status_code": "0", + "timetrial_data": timetrial_data, + # "past_season_timetrial_data": timetrial_data, + } + def handle_user_updatecartune_request(self, data: Dict, headers: Dict): user_id = headers["session"] From 8ccb7f08f24ca3a684f8c52d1c4123f4b695b124 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Tue, 3 Oct 2023 17:00:59 +0200 Subject: [PATCH 006/101] IDAC: Fixed frontend `user_id` not found --- titles/idac/frontend.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/titles/idac/frontend.py b/titles/idac/frontend.py index 32b1467..78abae8 100644 --- a/titles/idac/frontend.py +++ b/titles/idac/frontend.py @@ -94,14 +94,16 @@ class IDACFrontend(FE_Base): ) sesh: Session = request.getSession() usr_sesh = IUserSession(sesh) + user_id = usr_sesh.userId + # user_id = usr_sesh.user_id # profile export if uri.startswith("/game/idac/export"): - if usr_sesh.user_id == 0: + if user_id == 0: return redirectTo(b"/game/idac", request) # set the file name, content type and size to download the json - content = self.generate_all_tables_json(usr_sesh.user_id).encode("utf-8") + content = self.generate_all_tables_json(user_id).encode("utf-8") request.responseHeaders.addRawHeader( b"content-type", b"application/octet-stream" ) @@ -112,14 +114,14 @@ class IDACFrontend(FE_Base): b"content-length", str(len(content)).encode("utf-8") ) - self.logger.info(f"User {usr_sesh.user_id} exported their IDAC data") + self.logger.info(f"User {user_id} exported their IDAC data") return content profile_data, tickets, rank = None, None, None - if usr_sesh.user_id > 0: - profile_data = self.data.profile.get_profile(usr_sesh.user_id, self.version) - ticket_data = self.data.item.get_tickets(usr_sesh.user_id) - rank = self.data.profile.get_profile_rank(usr_sesh.user_id, self.version) + if user_id > 0: + profile_data = self.data.profile.get_profile(user_id, self.version) + ticket_data = self.data.item.get_tickets(user_id) + rank = self.data.profile.get_profile_rank(user_id, self.version) tickets = { self.ticket_names[ticket["ticket_id"]]: ticket["ticket_cnt"] From d55ada2538b4edac06d5655f6e98d3966a8d3e44 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Sun, 8 Oct 2023 19:56:04 +0200 Subject: [PATCH 007/101] IDAC: Improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- titles/idac/data/create_delivery_images.py | 8 ++++-- .../data/stamps/touhou_flandre_scarlet.json | Bin 10737 -> 10736 bytes .../data/stamps/touhou_remilia_scarlet.json | Bin 10695 -> 10694 bytes .../data/stamps/touhou_sakuya_izayoi.json | Bin 10684 -> 10683 bytes titles/idac/data/timeRelease_v0150.json | Bin 1354135 -> 1354811 bytes titles/idac/frontend/idac_index.jinja | 8 ++++-- titles/idac/schema/item.py | 27 ++++++++++++++---- titles/idac/season2.py | 4 +-- 8 files changed, 34 insertions(+), 13 deletions(-) diff --git a/titles/idac/data/create_delivery_images.py b/titles/idac/data/create_delivery_images.py index 196081e..ac37925 100644 --- a/titles/idac/data/create_delivery_images.py +++ b/titles/idac/data/create_delivery_images.py @@ -2,12 +2,13 @@ import os import hashlib -def prepare_images(image_folder="./images"): +def prepare_images(image_folder="titles/idac/data/images"): print(f"Preparing image delivery files in {image_folder}...") for file in os.listdir(image_folder): if file.endswith(".png") or file.endswith(".jpg"): - dpg_name = "adv-" + file[:-4].upper() + # dpg_name = "adv-" + file[:-4].upper() + dpg_name = file[:-4] if file.endswith(".png"): dpg_name += ".dpg" else: @@ -22,10 +23,11 @@ def prepare_images(image_folder="./images"): original_image = original_image_file.read() image_hash = hashlib.md5(original_image).hexdigest() print( - f"DPG for {file} not found, creating with hash {image_hash}..." + f"DPG for {file} not found, creating with hash {image_hash.upper()} ..." ) md5_buf = bytes.fromhex(image_hash) dpg_buf = md5_buf + original_image + dpg_name = "adv-" + image_hash.upper() + dpg_name[:-4] with open(os.path.join(image_folder, dpg_name), "wb") as dpg_file: dpg_file.write(dpg_buf) diff --git a/titles/idac/data/stamps/touhou_flandre_scarlet.json b/titles/idac/data/stamps/touhou_flandre_scarlet.json index 23d2a7d61f0b163210d92b70b806df91199addc0..da642f659648e2a507afbf9781cff478e7a22a53 100644 GIT binary patch delta 18 acmewu{2_RP>10KA;mPv_MK;Dx(F6cZ90xrB delta 16 Xcmewm{4scfDWlfpZb9{pF;g@FK9mNS diff --git a/titles/idac/data/stamps/touhou_remilia_scarlet.json b/titles/idac/data/stamps/touhou_remilia_scarlet.json index 3d917a9201e693a63746cef62ec21c03ea3f7d55..a6fe329374319b6804a918885cebfb9eec7edea2 100644 GIT binary patch delta 14 VcmX>ed@Oi^>EsSU#f{+wngB7X1?K<& delta 16 XcmX>Wd^~u9DWlfpc0t9BVFj81ITQvE diff --git a/titles/idac/data/stamps/touhou_sakuya_izayoi.json b/titles/idac/data/stamps/touhou_sakuya_izayoi.json index 7b98fb31486110eed551a2f2e07b631baec3516b..05d1e2877f3573663d16d43687d853f400f90978 100644 GIT binary patch delta 10 RcmdlJygPV;=|;0QO#mBi1bF}e delta 12 TcmdlTyeD{qDWld#(>6^2A_oMc diff --git a/titles/idac/data/timeRelease_v0150.json b/titles/idac/data/timeRelease_v0150.json index 02d6bf3ec0bdf8cf6f4136a6debb784303ae9950..694c81957b003e3c2a834dcabe8505174834ce7a 100644 GIT binary patch delta 333 zcmbP!IcWEdpbgzj(4%_l%ryj({=~f}>ChdY4WW11o zsGqO{Ny&lBOq`nqSOS@+Uod3l+^oe>%{YC7FgyF?6GCCr3$)m|H?QGrW|=-=KPSZ4 z3zA0D4L);nZ?@o@3v|aCZZ5FZAmRU~xVhn+fREfPjK-5M@@h>6$t@7!;of{-;1%bz zej!ev*%p(ZNGeWmScSxiKM3U95RI5#AR+=WbOWp8=0fQtq2{Z)?N@afftU%1nSq!E wh*^P{4T#x+m;;D8ftU-3xq+Amh42Ou*=N+%XC=Sq^hKX1cTUARa78x5XA11AV@es^#((NKvYyr!hZ!+ zLo%WQLDL%$3=-D^^a6Z{$G-33GU4-N{~>tvA803K&V!!NZdcEFzZKfs(1yIKMcS42 z)g4ubpSuycjmo+65|QtzWx(UUl%8lE@?4O1!xWW-Vqe-f!A;X3_ooWJ&J5vD@Xx1- zUHLfW)t9+H3(ig^_QQ0c{QDEN(O6g6rpf{kP=gR6sDlIr8VF)&fPn|F(1Zk9(EhHn H^mlm!u3$(m diff --git a/titles/idac/frontend/idac_index.jinja b/titles/idac/frontend/idac_index.jinja index 4bd10ca..eeecc65 100644 --- a/titles/idac/frontend/idac_index.jinja +++ b/titles/idac/frontend/idac_index.jinja @@ -2,7 +2,7 @@ {% block content %}

頭文字D THE ARCADE

-{% if sesh is defined and sesh["user_id"] > 0 %} +{% if sesh is defined and sesh["userId"] > 0 %}
@@ -27,6 +27,8 @@
Username

{{ profile.username }}

+
Cash
+

{{ profile.cash }} D

Grade

{% set grade = rank.grade %} @@ -37,7 +39,7 @@ {% else %} Unknown {% endif %} -

+
@@ -58,7 +60,7 @@
Mileage
-

{{ profile.mileage }} m

+

{{ profile.mileage / 1000}} km

{% if tickets is defined and tickets|length > 0 %} diff --git a/titles/idac/schema/item.py b/titles/idac/schema/item.py index ae49c74..e16bfa3 100644 --- a/titles/idac/schema/item.py +++ b/titles/idac/schema/item.py @@ -619,19 +619,36 @@ class IDACItemData(BaseData): def get_best_challenges_by_vs_type( self, aime_id: int, story_type: int = 4 ) -> Optional[List[Row]]: - sql = ( + subquery = ( select( challenge.c.story_type, + challenge.c.user, challenge.c.vs_type, - func.max(challenge.c.cleared_difficulty).label("max_clear_lv"), func.max(challenge.c.play_difficulty).label("last_play_lv"), - challenge.c.course_id, - challenge.c.play_count, ) .where( and_(challenge.c.user == aime_id, challenge.c.story_type == story_type) ) - .group_by(challenge.c.vs_type, challenge.c.course_id, challenge.c.play_count) + .group_by(challenge.c.vs_type) + ) + + sql = ( + select( + challenge.c.story_type, + challenge.c.vs_type, + challenge.c.cleared_difficulty.label("max_clear_lv"), + challenge.c.play_difficulty.label("last_play_lv"), + challenge.c.course_id, + challenge.c.play_count, + ) + .where( + and_( + challenge.c.user == subquery.c.user, + challenge.c.vs_type == subquery.c.vs_type, + challenge.c.play_difficulty == subquery.c.last_play_lv, + ), + ) + .order_by(challenge.c.vs_type) ) result = self.execute(sql) diff --git a/titles/idac/season2.py b/titles/idac/season2.py index 5717978..6ce6b37 100644 --- a/titles/idac/season2.py +++ b/titles/idac/season2.py @@ -146,9 +146,9 @@ class IDACSeason2(IDACBase): "round_event_exp": [], "stamp_info": self.stamp_info, # 0 = use default data, 1+ = server version of timereleasedata response - "timerelease_no": 1, + "timerelease_no": 2, # 0 = use default data, 1+ = server version of gachadata response - "timerelease_avatar_gacha_no": 1, + "timerelease_avatar_gacha_no": 2, "takeover_reward": [], "subcard_judge": [ { From 31188ccce6570b005d9afa4286edeb009b96fe8c Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Tue, 10 Oct 2023 16:06:58 +0200 Subject: [PATCH 008/101] IDAC: Hotfix to block amdaemon_emu users without correct access code --- titles/idac/season2.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/titles/idac/season2.py b/titles/idac/season2.py index 6ce6b37..537ea0d 100644 --- a/titles/idac/season2.py +++ b/titles/idac/season2.py @@ -342,16 +342,25 @@ class IDACSeason2(IDACBase): def handle_login_checklock_request(self, data: Dict, headers: Dict): user_id = data["id"] + access_code = data["accesscode"] + is_new_player = 0 - # check if an IDAC profile already exists - p = self.data.profile.get_profile(user_id, self.version) - is_new_player = 1 if p is None else 0 + # check that the user_id from access_code matches the user_id + if user_id == self.data.card.get_user_id_from_card(access_code): + lock_result = 1 + + # check if an IDAC profile already exists + p = self.data.profile.get_profile(user_id, self.version) + is_new_player = 1 if p is None else 0 + else: + lock_result = 0 + user_id = "" # other: in use return { "status_code": "0", # 0 = already in use, 1 = good, 2 = too new - "lock_result": 1, + "lock_result": lock_result, "lock_date": int(datetime.now().timestamp()), "daily_play": 1, "session": f"{user_id}", From 3ee1801ee6fc3effc6a1ca20d82929303df48555 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Tue, 10 Oct 2023 17:58:20 +0200 Subject: [PATCH 009/101] IDAC: Hotfix for play stamp and time trials --- .../data/stamps/touhou_remilia_scarlet.json | Bin 10694 -> 10694 bytes .../timetrial/touhou_flandre_scarlet.json | Bin 1189 -> 1163 bytes .../timetrial/touhou_remilia_scarlet.json | Bin 1183 -> 1157 bytes .../data/timetrial/touhou_sakuya_izayoi.json | Bin 1165 -> 1139 bytes 4 files changed, 0 insertions(+), 0 deletions(-) diff --git a/titles/idac/data/stamps/touhou_remilia_scarlet.json b/titles/idac/data/stamps/touhou_remilia_scarlet.json index a6fe329374319b6804a918885cebfb9eec7edea2..93369e8a86d46f0591fd8bdd975fd459f849b16c 100644 GIT binary patch delta 14 WcmX>Wd@OjwT}DQe&G#5zr~?2r&jyzO delta 14 WcmX>Wd@OjwT}DQ;&G#5zr~?2r+Xk5c diff --git a/titles/idac/data/timetrial/touhou_flandre_scarlet.json b/titles/idac/data/timetrial/touhou_flandre_scarlet.json index 314881aa584a8266de554ffc54ea2c4f79549fda..e7bab3d4bbf589aa4fbc1b7f042b054cd5c08455 100644 GIT binary patch delta 12 TcmZ3=+0D7ZfoXFTlQ$Cp8oL9L delta 12 TcmeC?T*|q@foZcEQz{bx8S4Xg diff --git a/titles/idac/data/timetrial/touhou_remilia_scarlet.json b/titles/idac/data/timetrial/touhou_remilia_scarlet.json index 35c855ef491ff50fc5006fa79068297b9986ebba..2414f4a125e537b8f102c07dea83bdb7300084ae 100644 GIT binary patch delta 12 TcmbQw*~+=WifMBQlQ$Cp8TA8s delta 16 XcmZqWoX@$zifOVg6YpkSrU)hgD7pkL diff --git a/titles/idac/data/timetrial/touhou_sakuya_izayoi.json b/titles/idac/data/timetrial/touhou_sakuya_izayoi.json index 9cf05850515d83b6ac5346c0350d2089a32bef0b..b817f5edfb0450c7f69d1fb602c8780881cfbb31 100644 GIT binary patch delta 12 TcmeC>{LHaIgK4u9lQ$Cp9GU~L delta 16 Xcmey&(aX6(gK4rf6YpkerU)hgEb# Date: Thu, 19 Oct 2023 18:52:10 +0200 Subject: [PATCH 010/101] IDAC: avatar gacha updated (HUGE PAIN), config fixes --- Dockerfile | 4 ++-- docs/game_specific_info.md | 5 +++-- example_config/idac.yaml | 6 +++--- titles/idac/data/avatarGacha.json | Bin 262275 -> 847892 bytes 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index e34dfec..d55c9e8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ 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 COPY requirements.txt requirements.txt @@ -15,7 +15,7 @@ COPY dbutils.py dbutils.py ADD core core ADD titles titles ADD config config -ADD log log +ADD logs logs ADD cert cert ENTRYPOINT [ "/app/entrypoint.sh" ] \ No newline at end of file diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index 615f10b..545a877 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -551,9 +551,9 @@ Config file is located in `config/idac.yaml`. | `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.enabled` | Enables/Disabled the play stamp events | +| `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.enabled` | Enables/Disables the time trial event | +| `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`) | @@ -642,6 +642,7 @@ python dbutils.py --game SDGT upgrade | 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. diff --git a/example_config/idac.yaml b/example_config/idac.yaml index 331ae82..626565e 100644 --- a/example_config/idac.yaml +++ b/example_config/idac.yaml @@ -1,5 +1,5 @@ server: - enabled: True + enable: True loglevel: "info" ssl: False ssl_key: "cert/idac.key" @@ -11,12 +11,12 @@ server: port_matching_p2p: 20003 stamp: - enabled: True + enable: True enabled_stamps: # max 3 play stamps - "touhou_remilia_scarlet" - "touhou_flandre_scarlet" - "touhou_sakuya_izayoi" timetrial: - enabled: True + enable: True enabled_timetrial: "touhou_remilia_scarlet" diff --git a/titles/idac/data/avatarGacha.json b/titles/idac/data/avatarGacha.json index 91bb516a9d64ff6a5aa4d1ac400e4214172c0f09..bcfba2f5def63c2d68f86b286d3afeaa8de2af02 100644 GIT binary patch literal 847892 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 From b5e3bf6f80b9d49ccbdbb413ab2e00eca5673f8d Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Thu, 19 Oct 2023 19:04:06 +0200 Subject: [PATCH 011/101] IDAC: Hotfix for UTF-8 encoding --- titles/idac/season2.py | 48 ++++++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/titles/idac/season2.py b/titles/idac/season2.py index 537ea0d..a5d9961 100644 --- a/titles/idac/season2.py +++ b/titles/idac/season2.py @@ -24,11 +24,11 @@ class IDACSeason2(IDACBase): self.logger.warning(f"Stamp {stamp} is enabled but does not exist!") continue - with open(f"./titles/idac/data/stamps/{stamp}.json", "rb") as f: + with open( + f"./titles/idac/data/stamps/{stamp}.json", encoding="UTF-8" + ) as f: self.logger.debug(f"Loading stamp {stamp}") - self.stamp_info.append( - self._fix_dates(json.loads(f.read().decode("utf-8"))) - ) + self.stamp_info.append(self._fix_dates(json.load(f))) self.timetrial_event = {} self.timetrial_event_id = None @@ -41,13 +41,16 @@ class IDACSeason2(IDACBase): ) else: self.logger.debug(f"Loading timetrial {timetrial}") - with open(f"./titles/idac/data/timetrial/{timetrial}.json", "rb") as f: - self.timetrial_event = self._fix_dates( - json.loads(f.read().decode("utf-8")) - ) + with open( + f"./titles/idac/data/timetrial/{timetrial}.json", + encoding="UTF-8", + ) as f: + self.timetrial_event = self._fix_dates(json.load(f)) # required for saving - self.timetrial_event_id = self.timetrial_event.get("timetrial_event_id") + self.timetrial_event_id = self.timetrial_event.get( + "timetrial_event_id" + ) def handle_alive_get_request(self, data: Dict, headers: Dict): return { @@ -108,9 +111,7 @@ class IDACSeason2(IDACBase): if self.core_cfg.server.is_develop: domain_api_game = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDGT/{ver_str}/" else: - domain_api_game = ( - f"http://{self.core_cfg.title.hostname}/SDGT/{ver_str}/" - ) + domain_api_game = f"http://{self.core_cfg.title.hostname}/SDGT/{ver_str}/" return { "status_code": "0", @@ -146,9 +147,9 @@ class IDACSeason2(IDACBase): "round_event_exp": [], "stamp_info": self.stamp_info, # 0 = use default data, 1+ = server version of timereleasedata response - "timerelease_no": 2, + "timerelease_no": 3, # 0 = use default data, 1+ = server version of gachadata response - "timerelease_avatar_gacha_no": 2, + "timerelease_avatar_gacha_no": 3, "takeover_reward": [], "subcard_judge": [ { @@ -214,7 +215,7 @@ class IDACSeason2(IDACBase): 18: Background """ - with open("./titles/idac/data/avatarGacha.json") as f: + with open("./titles/idac/data/avatarGacha.json", encoding="UTF-8") as f: avatar_gacha_data = json.load(f) # avatar_gacha_data = { @@ -1290,7 +1291,9 @@ class IDACSeason2(IDACBase): self.data.item.put_time_trial(self.version, user_id, data) # update the timetrial event points - self.data.item.put_timetrial_event(user_id, self.timetrial_event_id, event_point) + self.data.item.put_timetrial_event( + user_id, self.timetrial_event_id, event_point + ) return { "status_code": "0", @@ -1302,7 +1305,7 @@ class IDACSeason2(IDACBase): "timetrial_event_data": { "timetrial_event_id": self.timetrial_event_id, "point": event_point, - } + }, } def handle_user_updatestoryresult_request(self, data: Dict, headers: Dict): @@ -1598,7 +1601,9 @@ class IDACSeason2(IDACBase): skill_level_exp = course["skill_level_exp"] # get the best time for the current course for the current user - best_trial = self.data.item.get_time_trial_best_ranking_by_course(season_id, user_id, course_id) + best_trial = self.data.item.get_time_trial_best_ranking_by_course( + season_id, user_id, course_id + ) if not best_trial: continue @@ -1607,7 +1612,9 @@ class IDACSeason2(IDACBase): course_rank = self.data.item.get_time_trial_ranking_by_course( season_id, course_id, limit=None ) - course_rank = len([r for r in course_rank if r["goal_time"] < goal_time]) + 1 + course_rank = ( + len([r for r in course_rank if r["goal_time"] < goal_time]) + 1 + ) timetrial_data.append( { @@ -1617,7 +1624,8 @@ class IDACSeason2(IDACBase): "goal_time": goal_time, "rank": course_rank, "rank_dt": int(best_trial["play_dt"].timestamp()), - }) + } + ) return timetrial_data From 4fc4d98a746e025a7fccc1ef46bd8599fffd0e68 Mon Sep 17 00:00:00 2001 From: UncleJim Date: Thu, 26 Oct 2023 10:21:01 +0000 Subject: [PATCH 012/101] 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 Co-committed-by: UncleJim --- titles/idac/matching.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/idac/matching.py b/titles/idac/matching.py index fb9f56d..396eec8 100644 --- a/titles/idac/matching.py +++ b/titles/idac/matching.py @@ -55,7 +55,7 @@ class IDACMatching(resource.Resource): "host": self.game_config.server.matching_host, "port": self.game_config.server.matching_p2p, "room_name": "INDTA", - "state": self.get_matching_state(), + "state": 1, } self.logger.debug(f"Response {resp}") From 3f192bd84a2c80304e8719d7c24126bab6363ba4 Mon Sep 17 00:00:00 2001 From: UncleJim Date: Fri, 27 Oct 2023 11:05:55 +0000 Subject: [PATCH 013/101] 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 Co-committed-by: UncleJim --- titles/idac/season2.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/titles/idac/season2.py b/titles/idac/season2.py index a5d9961..13017b0 100644 --- a/titles/idac/season2.py +++ b/titles/idac/season2.py @@ -301,6 +301,11 @@ class IDACSeason2(IDACBase): # get the username, country and store from the profile profile = self.data.profile.get_profile(user_id, self.version) + arcade = self.data.arcade.get_arcade(profile["store"]) + + if arcade is None: + arcade = {} + arcade["name"] = self.core_cfg.server.name # should never happen if profile is None: @@ -313,9 +318,9 @@ class IDACSeason2(IDACBase): "username": profile["username"], "value": rank["goal_time"], # gat the store name from the profile - "store": self.core_cfg.server.name, + "store": arcade["name"], # get the country id from the profile, 9 is JPN - "country": 9, + "country": profile["country"], "style_car_id": rank["style_car_id"], # convert the datetime to a timestamp "play_dt": int(rank["play_dt"].timestamp()), @@ -492,11 +497,13 @@ class IDACSeason2(IDACBase): # get the user's profile, can never be None p = self.data.profile.get_profile(user_id, self.version) user_data = p._asdict() + arcade = self.data.arcade.get_arcade(user_data["store"]) + del user_data["id"] del user_data["user"] del user_data["version"] user_data["id"] = user_id - user_data["store_name"] = self.core_cfg.server.name + user_data["store_name"] = self.core_cfg.server.name if arcade is None else arcade["name"] user_data["last_play_date"] = int(user_data["last_play_date"].timestamp()) user_data["create_date"] = int(user_data["create_date"].timestamp()) @@ -1735,6 +1742,8 @@ class IDACSeason2(IDACBase): # set the name to CPU profile["username"] = f"CPU" # also reset stamps to default + profile["country"] = 9 + profile["store"] = 0 profile["stamp_key_assign_0"] = 0 profile["stamp_key_assign_1"] = 1 profile["stamp_key_assign_2"] = 2 @@ -1850,6 +1859,11 @@ class IDACSeason2(IDACBase): if time_trial: eval_id = time_trial["eval_id"] + arcade = self.data.arcade.get_arcade(profile["store"]) + if arcade is None: + arcade = {} + arcade["name"] = self.core_cfg.server.name + rival_data.append( { "id": profile["user"], @@ -1858,13 +1872,13 @@ class IDACSeason2(IDACBase): # only needed for power match "powerhouseLv": powerhose_lv, "mytitleId": profile["mytitle_id"], - "country": 9, + "country": profile["country"], "auraId": profile["aura_id"], "auraColor": profile["aura_color_id"], "auraLine": profile["aura_line_id"], # not sure? "roundRanking": 0, - "storeName": self.core_cfg.server.name, + "storeName": arcade["name"], "sex": avatar["sex"], "face": avatar["face"], "eye": avatar["eye"], From 1224baeade962c5174cbbccfbe81c662fcd60792 Mon Sep 17 00:00:00 2001 From: UncleJim Date: Mon, 6 Nov 2023 15:32:40 +0000 Subject: [PATCH 014/101] 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 Co-committed-by: UncleJim --- titles/idac/season2.py | 399 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 393 insertions(+), 6 deletions(-) diff --git a/titles/idac/season2.py b/titles/idac/season2.py index 13017b0..53818c8 100644 --- a/titles/idac/season2.py +++ b/titles/idac/season2.py @@ -141,7 +141,249 @@ class IDACSeason2(IDACBase): "domain_echo2": f"{self.core_cfg.title.hostname}:{self.game_config.server.echo2}", "domain_ping": f"{self.core_cfg.title.hostname}", "battle_gift_event_master": [], - "round_event": [], + "round_event":[ + { + "round_event_id":30, + "round_event_nm":"ARTEMiS Event", + "start_dt":int( + datetime.strptime("2023-01-01", "%Y-%m-%d").timestamp() + ), + "end_dt":int( + datetime.strptime("2029-01-01", "%Y-%m-%d").timestamp() + ), + "round_start_rank":0, + "save_filename":"Round_Event_djg_Or_dpg", + "vscount":[ + { + "reward_upper_limit":10, + "reward_lower_limit":10, + "reward":[ + { + "reward_category":1, + "reward_type":1 + } + ] + } + ], + "rank":[ + + ], + "point":[ + + ], + "playable_course_list":[ + { + "course_id":4, + "course_day":0 + }, + { + "course_id":4, + "course_day":1 + }, + { + "course_id":6, + "course_day":0 + }, + { + "course_id":6, + "course_day":1 + }, + { + "course_id":8, + "course_day":0 + }, + { + "course_id":8, + "course_day":1 + }, + { + "course_id":10, + "course_day":0 + }, + { + "course_id":10, + "course_day":1 + }, + { + "course_id":12, + "course_day":0 + }, + { + "course_id":12, + "course_day":1 + }, + { + "course_id":14, + "course_day":0 + }, + { + "course_id":14, + "course_day":1 + }, + { + "course_id":16, + "course_day":0 + }, + { + "course_id":16, + "course_day":1 + }, + { + "course_id":18, + "course_day":0 + }, + { + "course_id":18, + "course_day":1 + }, + { + "course_id":20, + "course_day":0 + }, + { + "course_id":20, + "course_day":1 + }, + { + "course_id":22, + "course_day":0 + }, + { + "course_id":22, + "course_day":1 + }, + { + "course_id":24, + "course_day":0 + }, + { + "course_id":24, + "course_day":1 + }, + { + "course_id":26, + "course_day":0 + }, + { + "course_id":26, + "course_day":1 + }, + { + "course_id":36, + "course_day":0 + }, + { + "course_id":36, + "course_day":1 + }, + { + "course_id":38, + "course_day":0 + }, + { + "course_id":38, + "course_day":1 + }, + { + "course_id":40, + "course_day":0 + }, + { + "course_id":40, + "course_day":1 + }, + { + "course_id":42, + "course_day":0 + }, + { + "course_id":42, + "course_day":1 + }, + { + "course_id":44, + "course_day":0 + }, + { + "course_id":44, + "course_day":1 + }, + { + "course_id":46, + "course_day":0 + }, + { + "course_id":46, + "course_day":1 + }, + { + "course_id":48, + "course_day":0 + }, + { + "course_id":48, + "course_day":1 + }, + { + "course_id":50, + "course_day":0 + }, + { + "course_id":50, + "course_day":1 + }, + { + "course_id":52, + "course_day":0 + }, + { + "course_id":52, + "course_day":1 + }, + { + "course_id":54, + "course_day":0 + }, + { + "course_id":54, + "course_day":1 + }, + { + "course_id":56, + "course_day":0 + }, + { + "course_id":56, + "course_day":1 + }, + { + "course_id":58, + "course_day":0 + }, + { + "course_id":58, + "course_day":1 + }, + { + "course_id":68, + "course_day":0 + }, + { + "course_id":68, + "course_day":1 + }, + { + "course_id":70, + "course_day":0 + }, + { + "course_id":70, + "course_day":1 + } + ] + } + ], + "last_round_event": [], "last_round_event_ranking": [], "round_event_exp": [], @@ -1460,7 +1702,7 @@ class IDACSeason2(IDACBase): # get the vs use count from database and update it style_car_id = data.pop("style_car_id") car_data = self.data.item.get_car(user_id, self.version, style_car_id) - vs_use_count = car_data["vs_use_count"] + 1 + story_use_count = car_data["story_use_count"] + 1 # save car data in database self.data.item.put_car( @@ -1469,7 +1711,7 @@ class IDACSeason2(IDACBase): { "style_car_id": style_car_id, "car_mileage": data.pop("car_mileage"), - "vs_use_count": vs_use_count, + "story_use_count": story_use_count, }, ) @@ -1528,7 +1770,7 @@ class IDACSeason2(IDACBase): # get the vs use count from database and update it style_car_id = data.get("style_car_id") car_data = self.data.item.get_car(user_id, self.version, style_car_id) - vs_use_count = car_data["vs_use_count"] + 1 + story_use_count = car_data["story_use_count"] + 1 # save car data in database self.data.item.put_car( @@ -1537,7 +1779,7 @@ class IDACSeason2(IDACBase): { "style_car_id": style_car_id, "car_mileage": data.pop("car_mileage"), - "vs_use_count": vs_use_count, + "story_use_count": story_use_count, }, ) @@ -2115,6 +2357,151 @@ class IDACSeason2(IDACBase): "win_count_multi": win_count_multi, } + def handle_timetrial_getbestrecordprebattle_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + course_pickup_car_best_data = [] + courses = self.data.item.get_time_trial_courses(self.version) + for course in courses: + car_list = [] + best_cars = self.data.item.get_time_trial_best_cars_by_course( + self.version, user_id, course["course_id"] + ) + + for car in best_cars: + car_list.append( + { + "rank": 1, + # no clue + "member": user_id, + "value": car["goal_time"], + "store": self.core_cfg.server.name, + # use car_id from request? + "car_id": 0, + "style_car_id": car["style_car_id"], + "play_dt": car["play_dt"].timestamp(), + "section_time_1": car["section_time_1"], + "section_time_2": car["section_time_2"], + "section_time_3": car["section_time_3"], + "section_time_4": car["section_time_4"], + "mission": car["mission"], + } + ) + + course_pickup_car_best_data.append( + { + "course_id": course["course_id"], + "car_list": car_list, + } + ) + + return { + "status_code": "0", + # "course_car_best_data": course_car_best_data, + # "course_best_data": course_best_data, + "course_pickup_car_best_data": course_pickup_car_best_data, + } + + + def handle_user_updateonlinebattle_request(self, data: Dict, headers: Dict): + return { + "status_code": "0", + "bothwin_penalty": 1, + } + + def handle_user_updateonlinebattleresult_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + stock_data: Dict = data.pop("stock_obj") + # save stock data in database + self._save_stock_data(user_id, stock_data) + + ticket_data: List = data.pop("ticket_data") + for ticket in ticket_data: + self.data.item.put_ticket(user_id, ticket) + + reward_dist_data: Dict = data.pop("reward_dist_obj") + rank_data: Dict = data.pop("mode_rank_obj") + + # save rank dist data in database + rank_data.update(reward_dist_data) + self.data.profile.put_profile_rank(user_id, self.version, rank_data) + + driver_debut_data = data.pop("driver_debut_obj") + + # get the use_count and net_vs_use_count of the used car + style_car_id = data.get("style_car_id") + car_mileage = data.pop("car_mileage") + used_car = self.data.item.get_car(user_id, self.version, style_car_id)._asdict() + + # increase the use_count and net_vs_use_count of the used car + used_car["use_count"] += 1 + used_car["net_vs_use_count"] += 1 + used_car["car_mileage"] = car_mileage + + # save the used car in database + self.data.item.put_car(user_id, self.version, used_car) + + # get the profile data, update total_play and daily_play, and save it + profile = self.data.profile.get_profile(user_id, self.version) + total_play = profile["total_play"] + 1 + + # save the profile in database + self.data.profile.put_profile( + user_id, + self.version, + { + "total_play": total_play, + "last_play_date": datetime.now(), + "mileage": data.pop("mileage"), + "aura_id": data.pop("aura_id"), + "aura_color_id": data.pop("aura_color_id"), + "aura_line_id": data.pop("aura_line_id"), + "cash": data.pop("cash"), + "total_cash": data.pop("total_cash"), + "dressup_point": data.pop("dressup_point"), + "avatar_point": data.pop("avatar_point"), + }, + ) + + self.data.item.put_vs_info(user_id, data) + + vs_info = { + "battle_mode": 0, + "vs_cnt": 1, + "vs_win": data.get("win_flg"), + "invalid": 0, + "str": 0, + "str_now": 0, + "lose_now": 0, + "vs_history": data.get("vs_history"), + "course_select_priority": data.get("course_select_priority"), + "vsinfo_course_data": [ + { + "course_id": data.get("course_id"), + "vs_cnt": 1, + "vs_win": data.get("win_flg"), + } + ], + } + + return { + "status_code": "0", + "vsinfo_data": vs_info, + "round_event": [ + { + "count": 1, + "win": 1, + "rank": 1, + "point": 1, + "total_round_point": 1, + } + ], + "car_use_count": [], + "maker_use_count": [], + } + + def handle_user_updatestorebattleresult_request(self, data: Dict, headers: Dict): user_id = headers["session"] @@ -2147,7 +2534,7 @@ class IDACSeason2(IDACBase): # increase the use_count and net_vs_use_count of the used car used_car["use_count"] += 1 - used_car["net_vs_use_count"] += 1 + used_car["vs_use_count"] += 1 used_car["car_mileage"] = car_mileage # save the used car in database From acac2c46706a8852dde3abfc69b56d05a99f510d Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Mon, 6 Nov 2023 18:11:30 +0100 Subject: [PATCH 015/101] IDAC: time trial should display car time now --- titles/idac/base.py | 6 +- titles/idac/schema/item.py | 10 +- titles/idac/season2.py | 452 ++++++++++++++----------------------- 3 files changed, 181 insertions(+), 287 deletions(-) diff --git a/titles/idac/base.py b/titles/idac/base.py index c556aff..eb75b93 100644 --- a/titles/idac/base.py +++ b/titles/idac/base.py @@ -7,10 +7,10 @@ from titles.idac.database import IDACData class IDACBase: - def __init__(self, cfg: CoreConfig, game_cfg: IDACConfig) -> None: - self.core_cfg = cfg + def __init__(self, core_cfg: CoreConfig, game_cfg: IDACConfig) -> None: + self.core_cfg = core_cfg self.game_config = game_cfg self.game = IDACConstants.GAME_CODE self.version = IDACConstants.VER_IDAC_SEASON_1 - self.data = IDACData(cfg) + self.data = IDACData(core_cfg) self.logger = logging.getLogger("idac") diff --git a/titles/idac/schema/item.py b/titles/idac/schema/item.py index e16bfa3..80ee7ba 100644 --- a/titles/idac/schema/item.py +++ b/titles/idac/schema/item.py @@ -497,7 +497,7 @@ class IDACItemData(BaseData): return result.fetchall() def get_time_trial_best_cars_by_course( - self, version: int, aime_id: int, course_id: int + self, version: int, course_id: int, aime_id: Optional[int] = None ) -> Optional[List[Row]]: subquery = ( select( @@ -508,14 +508,16 @@ class IDACItemData(BaseData): .where( and_( trial.c.version == version, - trial.c.user == aime_id, trial.c.course_id == course_id, ) ) - .group_by(trial.c.style_car_id) - .subquery() ) + if aime_id is not None: + subquery = subquery.where(trial.c.user == aime_id) + + subquery = subquery.group_by(trial.c.style_car_id).subquery() + sql = select(trial).where( and_( trial.c.version == subquery.c.version, diff --git a/titles/idac/season2.py b/titles/idac/season2.py index 53818c8..61109da 100644 --- a/titles/idac/season2.py +++ b/titles/idac/season2.py @@ -141,249 +141,83 @@ class IDACSeason2(IDACBase): "domain_echo2": f"{self.core_cfg.title.hostname}:{self.game_config.server.echo2}", "domain_ping": f"{self.core_cfg.title.hostname}", "battle_gift_event_master": [], - "round_event":[ + "round_event": [ { - "round_event_id":30, - "round_event_nm":"ARTEMiS Event", - "start_dt":int( - datetime.strptime("2023-01-01", "%Y-%m-%d").timestamp() - ), - "end_dt":int( - datetime.strptime("2029-01-01", "%Y-%m-%d").timestamp() - ), - "round_start_rank":0, - "save_filename":"Round_Event_djg_Or_dpg", - "vscount":[ + "round_event_id": 30, + "round_event_nm": f"{self.core_cfg.server.name} Event", + "start_dt": int( + datetime.strptime("2023-01-01", "%Y-%m-%d").timestamp() + ), + "end_dt": int( + datetime.strptime("2029-01-01", "%Y-%m-%d").timestamp() + ), + "round_start_rank": 0, + "save_filename": "Round_Event_djg_Or_dpg", + "vscount": [ { - "reward_upper_limit":10, - "reward_lower_limit":10, - "reward":[ - { - "reward_category":1, - "reward_type":1 - } - ] + "reward_upper_limit": 10, + "reward_lower_limit": 10, + "reward": [{"reward_category": 1, "reward_type": 1}], } ], - "rank":[ - + "rank": [], + "point": [], + "playable_course_list": [ + {"course_id": 4, "course_day": 0}, + {"course_id": 4, "course_day": 1}, + {"course_id": 6, "course_day": 0}, + {"course_id": 6, "course_day": 1}, + {"course_id": 8, "course_day": 0}, + {"course_id": 8, "course_day": 1}, + {"course_id": 10, "course_day": 0}, + {"course_id": 10, "course_day": 1}, + {"course_id": 12, "course_day": 0}, + {"course_id": 12, "course_day": 1}, + {"course_id": 14, "course_day": 0}, + {"course_id": 14, "course_day": 1}, + {"course_id": 16, "course_day": 0}, + {"course_id": 16, "course_day": 1}, + {"course_id": 18, "course_day": 0}, + {"course_id": 18, "course_day": 1}, + {"course_id": 20, "course_day": 0}, + {"course_id": 20, "course_day": 1}, + {"course_id": 22, "course_day": 0}, + {"course_id": 22, "course_day": 1}, + {"course_id": 24, "course_day": 0}, + {"course_id": 24, "course_day": 1}, + {"course_id": 26, "course_day": 0}, + {"course_id": 26, "course_day": 1}, + {"course_id": 36, "course_day": 0}, + {"course_id": 36, "course_day": 1}, + {"course_id": 38, "course_day": 0}, + {"course_id": 38, "course_day": 1}, + {"course_id": 40, "course_day": 0}, + {"course_id": 40, "course_day": 1}, + {"course_id": 42, "course_day": 0}, + {"course_id": 42, "course_day": 1}, + {"course_id": 44, "course_day": 0}, + {"course_id": 44, "course_day": 1}, + {"course_id": 46, "course_day": 0}, + {"course_id": 46, "course_day": 1}, + {"course_id": 48, "course_day": 0}, + {"course_id": 48, "course_day": 1}, + {"course_id": 50, "course_day": 0}, + {"course_id": 50, "course_day": 1}, + {"course_id": 52, "course_day": 0}, + {"course_id": 52, "course_day": 1}, + {"course_id": 54, "course_day": 0}, + {"course_id": 54, "course_day": 1}, + {"course_id": 56, "course_day": 0}, + {"course_id": 56, "course_day": 1}, + {"course_id": 58, "course_day": 0}, + {"course_id": 58, "course_day": 1}, + {"course_id": 68, "course_day": 0}, + {"course_id": 68, "course_day": 1}, + {"course_id": 70, "course_day": 0}, + {"course_id": 70, "course_day": 1}, ], - "point":[ - - ], - "playable_course_list":[ - { - "course_id":4, - "course_day":0 - }, - { - "course_id":4, - "course_day":1 - }, - { - "course_id":6, - "course_day":0 - }, - { - "course_id":6, - "course_day":1 - }, - { - "course_id":8, - "course_day":0 - }, - { - "course_id":8, - "course_day":1 - }, - { - "course_id":10, - "course_day":0 - }, - { - "course_id":10, - "course_day":1 - }, - { - "course_id":12, - "course_day":0 - }, - { - "course_id":12, - "course_day":1 - }, - { - "course_id":14, - "course_day":0 - }, - { - "course_id":14, - "course_day":1 - }, - { - "course_id":16, - "course_day":0 - }, - { - "course_id":16, - "course_day":1 - }, - { - "course_id":18, - "course_day":0 - }, - { - "course_id":18, - "course_day":1 - }, - { - "course_id":20, - "course_day":0 - }, - { - "course_id":20, - "course_day":1 - }, - { - "course_id":22, - "course_day":0 - }, - { - "course_id":22, - "course_day":1 - }, - { - "course_id":24, - "course_day":0 - }, - { - "course_id":24, - "course_day":1 - }, - { - "course_id":26, - "course_day":0 - }, - { - "course_id":26, - "course_day":1 - }, - { - "course_id":36, - "course_day":0 - }, - { - "course_id":36, - "course_day":1 - }, - { - "course_id":38, - "course_day":0 - }, - { - "course_id":38, - "course_day":1 - }, - { - "course_id":40, - "course_day":0 - }, - { - "course_id":40, - "course_day":1 - }, - { - "course_id":42, - "course_day":0 - }, - { - "course_id":42, - "course_day":1 - }, - { - "course_id":44, - "course_day":0 - }, - { - "course_id":44, - "course_day":1 - }, - { - "course_id":46, - "course_day":0 - }, - { - "course_id":46, - "course_day":1 - }, - { - "course_id":48, - "course_day":0 - }, - { - "course_id":48, - "course_day":1 - }, - { - "course_id":50, - "course_day":0 - }, - { - "course_id":50, - "course_day":1 - }, - { - "course_id":52, - "course_day":0 - }, - { - "course_id":52, - "course_day":1 - }, - { - "course_id":54, - "course_day":0 - }, - { - "course_id":54, - "course_day":1 - }, - { - "course_id":56, - "course_day":0 - }, - { - "course_id":56, - "course_day":1 - }, - { - "course_id":58, - "course_day":0 - }, - { - "course_id":58, - "course_day":1 - }, - { - "course_id":68, - "course_day":0 - }, - { - "course_id":68, - "course_day":1 - }, - { - "course_id":70, - "course_day":0 - }, - { - "course_id":70, - "course_day":1 - } - ] } ], - "last_round_event": [], "last_round_event_ranking": [], "round_event_exp": [], @@ -740,12 +574,14 @@ class IDACSeason2(IDACBase): p = self.data.profile.get_profile(user_id, self.version) user_data = p._asdict() arcade = self.data.arcade.get_arcade(user_data["store"]) - + del user_data["id"] del user_data["user"] del user_data["version"] user_data["id"] = user_id - user_data["store_name"] = self.core_cfg.server.name if arcade is None else arcade["name"] + user_data["store_name"] = ( + self.core_cfg.server.name if arcade is None else arcade["name"] + ) user_data["last_play_date"] = int(user_data["last_play_date"].timestamp()) user_data["create_date"] = int(user_data["create_date"].timestamp()) @@ -1066,13 +902,13 @@ class IDACSeason2(IDACBase): for course in courses: car_list = [] best_cars = self.data.item.get_time_trial_best_cars_by_course( - self.version, user_id, course["course_id"] + self.version, course["course_id"], user_id ) - for car in best_cars: + for i, car in enumerate(best_cars): car_list.append( { - "rank": 1, + "rank": i + 1, # no clue "member": user_id, "value": car["goal_time"], @@ -1099,39 +935,97 @@ class IDACSeason2(IDACBase): return { "status_code": "0", "course_mybest_data": course_mybest_data, - # "course_car_best_data": course_car_best_data, - # "course_best_data": course_best_data, - "location_course_store_best_data": [], "course_pickup_car_best_data": course_pickup_car_best_data, } def handle_timetrial_getbestrecordprerace_request(self, data: Dict, headers: Dict): user_id = headers["session"] - for car_id in data["car_ids"]: - pass + course_id = data["course_id"] + for car in data["car_ids"]: + # TODO: get the best record for this car + style_car_id = car["style_car_id"] - course_best_data = [ - { - "course_id": 56, - "rank": 1, - "member": user_id, - "value": 118016, - "store": self.core_cfg.server.name, - "car_id": 0, - "style_car_id": 0, - "play_dt": int(datetime.now().timestamp() - 86400), - "section_time_1": 45205, - "section_time_2": 47779, - "section_time_3": 43480, - "section_time_4": 41552, - "mission": 1, - } - ] + # Not sure if this is actually correct + ranking = self.data.item.get_time_trial_ranking_by_course( + self.version, course_id + ) + course_best_data = [] + for i, rank in enumerate(ranking): + car_user_id = rank["user"] + + # get the username, country and store from the profile + profile = self.data.profile.get_profile(car_user_id, self.version) + arcade = self.data.arcade.get_arcade(profile["store"]) + + if arcade is None: + arcade = {} + arcade["name"] = self.core_cfg.server.name + + # should never happen + if profile is None: + continue + + course_best_data.append( + { + "course_id": course_id, + "rank": i + 1, + "member": car_user_id, + "value": rank["goal_time"], + "store": arcade["name"], + # use car_id from request? + "car_id": 0, + "style_car_id": rank["style_car_id"], + "play_dt": rank["play_dt"].timestamp(), + "section_time_1": rank["section_time_1"], + "section_time_2": rank["section_time_2"], + "section_time_3": rank["section_time_3"], + "section_time_4": rank["section_time_4"], + "mission": rank["mission"], + } + ) + + best_cars = self.data.item.get_time_trial_best_cars_by_course( + self.version, course_id + ) + + car_list = [] + for i, rank in enumerate(best_cars): + car_user_id = rank["user"] + # get the username, country and store from the profile + profile = self.data.profile.get_profile(car_user_id, self.version) + arcade = self.data.arcade.get_arcade(profile["store"]) + + if arcade is None: + arcade = {} + arcade["name"] = self.core_cfg.server.name + + # should never happen + if profile is None: + continue + + car_list.append( + { + "rank": i + 1, + # no clue + "member": car_user_id, + "value": rank["goal_time"], + "store": arcade["name"], + # use car_id from request? + "car_id": 0, + "style_car_id": rank["style_car_id"], + "play_dt": rank["play_dt"].timestamp(), + "section_time_1": rank["section_time_1"], + "section_time_2": rank["section_time_2"], + "section_time_3": rank["section_time_3"], + "section_time_4": rank["section_time_4"], + "mission": rank["mission"], + } + ) return { "status_code": "0", - # "course_car_best_data": course_car_best_data, + "course_car_best_data": [{"course_id": course_id, "car_list": car_list}], "course_best_data": course_best_data, } @@ -2357,7 +2251,9 @@ class IDACSeason2(IDACBase): "win_count_multi": win_count_multi, } - def handle_timetrial_getbestrecordprebattle_request(self, data: Dict, headers: Dict): + def handle_timetrial_getbestrecordprebattle_request( + self, data: Dict, headers: Dict + ): user_id = headers["session"] course_pickup_car_best_data = [] @@ -2365,13 +2261,13 @@ class IDACSeason2(IDACBase): for course in courses: car_list = [] best_cars = self.data.item.get_time_trial_best_cars_by_course( - self.version, user_id, course["course_id"] + self.version, course["course_id"], user_id ) - for car in best_cars: + for i, car in enumerate(best_cars): car_list.append( { - "rank": 1, + "rank": i + 1, # no clue "member": user_id, "value": car["goal_time"], @@ -2397,13 +2293,10 @@ class IDACSeason2(IDACBase): return { "status_code": "0", - # "course_car_best_data": course_car_best_data, - # "course_best_data": course_best_data, "course_pickup_car_best_data": course_pickup_car_best_data, } - - def handle_user_updateonlinebattle_request(self, data: Dict, headers: Dict): + def handle_user_updateonlinebattle_request(self, data: Dict, headers: Dict): return { "status_code": "0", "bothwin_penalty": 1, @@ -2415,20 +2308,20 @@ class IDACSeason2(IDACBase): stock_data: Dict = data.pop("stock_obj") # save stock data in database self._save_stock_data(user_id, stock_data) - + ticket_data: List = data.pop("ticket_data") for ticket in ticket_data: self.data.item.put_ticket(user_id, ticket) - + reward_dist_data: Dict = data.pop("reward_dist_obj") rank_data: Dict = data.pop("mode_rank_obj") # save rank dist data in database rank_data.update(reward_dist_data) self.data.profile.put_profile_rank(user_id, self.version, rank_data) - + driver_debut_data = data.pop("driver_debut_obj") - + # get the use_count and net_vs_use_count of the used car style_car_id = data.get("style_car_id") car_mileage = data.pop("car_mileage") @@ -2441,7 +2334,7 @@ class IDACSeason2(IDACBase): # save the used car in database self.data.item.put_car(user_id, self.version, used_car) - + # get the profile data, update total_play and daily_play, and save it profile = self.data.profile.get_profile(user_id, self.version) total_play = profile["total_play"] + 1 @@ -2463,7 +2356,7 @@ class IDACSeason2(IDACBase): "avatar_point": data.pop("avatar_point"), }, ) - + self.data.item.put_vs_info(user_id, data) vs_info = { @@ -2484,7 +2377,7 @@ class IDACSeason2(IDACBase): } ], } - + return { "status_code": "0", "vsinfo_data": vs_info, @@ -2501,7 +2394,6 @@ class IDACSeason2(IDACBase): "maker_use_count": [], } - def handle_user_updatestorebattleresult_request(self, data: Dict, headers: Dict): user_id = headers["session"] From 4c3852d6d40eb7716c5f3740960b11f2d9e66bbc Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Mon, 6 Nov 2023 19:54:13 +0100 Subject: [PATCH 016/101] IDAC: Added round event "Season 2 2nd" --- titles/idac/season2.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/titles/idac/season2.py b/titles/idac/season2.py index 61109da..ca57392 100644 --- a/titles/idac/season2.py +++ b/titles/idac/season2.py @@ -152,12 +152,33 @@ class IDACSeason2(IDACBase): datetime.strptime("2029-01-01", "%Y-%m-%d").timestamp() ), "round_start_rank": 0, - "save_filename": "Round_Event_djg_Or_dpg", + "save_filename": "0", + # https://info-initialdac.sega.jp/1898/ "vscount": [ { "reward_upper_limit": 10, "reward_lower_limit": 10, - "reward": [{"reward_category": 1, "reward_type": 1}], + "reward": [{"reward_category": 21, "reward_type": 483}], + }, + { + "reward_upper_limit": 40, + "reward_lower_limit": 40, + "reward": [{"reward_category": 21, "reward_type": 484}], + }, + { + "reward_upper_limit": 80, + "reward_lower_limit": 80, + "reward": [{"reward_category": 22, "reward_type": 516}], + }, + { + "reward_upper_limit": 120, + "reward_lower_limit": 120, + "reward": [{"reward_category": 21, "reward_type": 461}], + }, + { + "reward_upper_limit": 180, + "reward_lower_limit": 180, + "reward": [{"reward_category": 21, "reward_type": 462}], } ], "rank": [], From 94c326a27dfb5944866f7b057c1ca4740845dad7 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Wed, 8 Nov 2023 21:42:53 -0500 Subject: [PATCH 017/101] ongeki: add option to use https for red and beyond --- example_config/ongeki.yaml | 1 + titles/ongeki/config.py | 6 ++++++ titles/ongeki/index.py | 17 ++++++++++------- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/example_config/ongeki.yaml b/example_config/ongeki.yaml index e4088c0..9af5efe 100644 --- a/example_config/ongeki.yaml +++ b/example_config/ongeki.yaml @@ -1,6 +1,7 @@ server: enable: True loglevel: "info" + use_https: False gachas: enabled_gachas: diff --git a/titles/ongeki/config.py b/titles/ongeki/config.py index 2321af6..b952b2d 100644 --- a/titles/ongeki/config.py +++ b/titles/ongeki/config.py @@ -21,6 +21,12 @@ class OngekiServerConfig: self.__config, "ongeki", "server", "loglevel", default="info" ) ) + + @property + def use_https(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "ongeki", "server", "use_https", default=False + ) class OngekiGachaConfig: diff --git a/titles/ongeki/index.py b/titles/ongeki/index.py index 2e928f6..bdfd8d5 100644 --- a/titles/ongeki/index.py +++ b/titles/ongeki/index.py @@ -127,15 +127,18 @@ class OngekiServlet(BaseServlet): ) 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}/", - f"{self.core_cfg.title.hostname}:{Utils.get_title_port(self.core_cfg)}/", - ) + title_port_int = Utils.get_title_port(self.core_cfg) + proto = "https" if self.game_cfg.server.use_https and game_ver >= 120 else "http" + + if proto == "https": + t_port = f":{title_port_int}" if title_port_int != 443 and not self.core_cfg.server.is_using_proxy else "" + + else: + t_port = f":{title_port_int}" if title_port_int != 80 and not self.core_cfg.server.is_using_proxy else "" return ( - f"http://{self.core_cfg.title.hostname}/{game_code}/{game_ver}/", - f"{self.core_cfg.title.hostname}/", + f"{proto}://{self.core_cfg.title.hostname}{t_port}/{game_code}/{game_ver}/", + f"{self.core_cfg.title.hostname}{t_port}/", ) def render_POST(self, request: Request, game_code: str, matchers: Dict) -> bytes: From 4b22bd734e728b0ddcb1142bb17be7343fca9771 Mon Sep 17 00:00:00 2001 From: Midorica Date: Wed, 8 Nov 2023 23:36:26 -0500 Subject: [PATCH 018/101] fixing handle_data for Crossbeats --- titles/cxb/index.py | 45 ++++++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/titles/cxb/index.py b/titles/cxb/index.py index 7e6cd7c..54e2401 100644 --- a/titles/cxb/index.py +++ b/titles/cxb/index.py @@ -124,35 +124,42 @@ class CxbServlet(BaseServlet): func_to_find = "handle_data_" version_string = "Base" internal_ver = 0 + version = 0 if req_json == {}: self.logger.warning(f"Empty json request to /data") return b"" + + subcmd = list(req_json.keys())[0] + if subcmd == "dldate": - if ( - not type(req_json["dldate"]) is dict - or "filetype" not in req_json["dldate"] - ): - self.logger.warning(f"Malformed dldate request: {req_json}") - return b"" + if ( + not type(req_json["dldate"]) is dict + or "filetype" not in req_json["dldate"] + ): + self.logger.warning(f"Malformed dldate request: {req_json}") + return b"" - filetype = req_json["dldate"]["filetype"] - filetype_split = filetype.split("/") + filetype = req_json["dldate"]["filetype"] + filetype_split = filetype.split("/") - if len(filetype_split) < 2 or not filetype_split[0].isnumeric(): - self.logger.warning(f"Malformed dldate request: {req_json}") - return b"" + if len(filetype_split) < 2 or not filetype_split[0].isnumeric(): + self.logger.warning(f"Malformed dldate request: {req_json}") + return b"" - version = int(filetype_split[0]) - filename = filetype_split[len(filetype_split) - 1] + version = int(filetype_split[0]) + filename = filetype_split[len(filetype_split) - 1] - match = re.match( - "^([A-Za-z]*)(\d\d\d\d)$", filetype_split[len(filetype_split) - 1] - ) - if match: - func_to_find += f"{inflection.underscore(match.group(1))}xxxx" + match = re.match( + "^([A-Za-z]*)(\d\d\d\d)$", filetype_split[len(filetype_split) - 1] + ) + if match: + func_to_find += f"{inflection.underscore(match.group(1))}xxxx" + else: + func_to_find += f"{inflection.underscore(filename)}" else: - func_to_find += f"{inflection.underscore(filename)}" + filetype = subcmd + func_to_find += filetype func_to_find += "_request" From eb10bc2560b14992ab900491bbe469be639fdbd7 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Wed, 8 Nov 2023 23:54:35 -0500 Subject: [PATCH 019/101] idz: fix news url --- titles/idz/handlers/load_server_info.py | 7 +++++-- titles/idz/index.py | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/titles/idz/handlers/load_server_info.py b/titles/idz/handlers/load_server_info.py index ef6e81c..4c60dd7 100644 --- a/titles/idz/handlers/load_server_info.py +++ b/titles/idz/handlers/load_server_info.py @@ -1,5 +1,6 @@ import struct +from core.utils import Utils from .base import IDZHandlerBase from core.config import CoreConfig from ..config import IDZConfig @@ -20,9 +21,11 @@ class IDZHandlerLoadServerInfo(IDZHandlerBase): offset = 0 if self.version >= IDZConstants.VER_IDZ_210: offset = 2 + + t_port = Utils.get_title_port(self.core_config) - news_str = f"http://{self.core_config.title.hostname}:{self.core_config.title.port}/SDDF/230/news/news80**.txt" - err_str = f"http://{self.core_config.title.hostname}:{self.core_config.title.port}/SDDF/230/error" + news_str = f"http://{self.core_config.title.hostname}:{t_port}/idz/news/news80**.txt" + err_str = f"http://{self.core_config.title.hostname}:{t_port}/idz/error" len_hostname = len(self.core_config.title.hostname) len_news = len(news_str) diff --git a/titles/idz/index.py b/titles/idz/index.py index 39d21c2..62473f9 100644 --- a/titles/idz/index.py +++ b/titles/idz/index.py @@ -85,7 +85,8 @@ class IDZServlet(BaseServlet): def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: return[ - [("render_GET", "/{game}/{version}/{endpoint:.*?}", {'game': R'S...'})], # TODO: Slim this down to only the news stuff + [("render_GET", "/idz/news/{endpoint:.*?}", {}), + ("render_GET", "/idz/error", {})], [] ] From e769404e16d132c8f00fa4ad51cee6a7cbf43d43 Mon Sep 17 00:00:00 2001 From: Midorica Date: Thu, 9 Nov 2023 00:06:15 -0500 Subject: [PATCH 020/101] fixing TLS support for ongeki --- titles/ongeki/index.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/titles/ongeki/index.py b/titles/ongeki/index.py index bdfd8d5..5f318b6 100644 --- a/titles/ongeki/index.py +++ b/titles/ongeki/index.py @@ -128,10 +128,11 @@ class OngekiServlet(BaseServlet): def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]: title_port_int = Utils.get_title_port(self.core_cfg) + title_port_ssl_int = Utils.get_title_port_ssl(self.core_cfg) proto = "https" if self.game_cfg.server.use_https and game_ver >= 120 else "http" if proto == "https": - t_port = f":{title_port_int}" if title_port_int != 443 and not self.core_cfg.server.is_using_proxy else "" + t_port = f":{title_port_ssl_int}" if title_port_ssl_int != 443 and not self.core_cfg.server.is_using_proxy else "" else: t_port = f":{title_port_int}" if title_port_int != 80 and not self.core_cfg.server.is_using_proxy else "" From 4047a383702b70356853727e290d47aaa0da1531 Mon Sep 17 00:00:00 2001 From: Midorica Date: Thu, 9 Nov 2023 00:10:19 -0500 Subject: [PATCH 021/101] adding encryption version check for Ongeki --- titles/ongeki/index.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/titles/ongeki/index.py b/titles/ongeki/index.py index 5f318b6..26686a2 100644 --- a/titles/ongeki/index.py +++ b/titles/ongeki/index.py @@ -208,6 +208,7 @@ class OngekiServlet(BaseServlet): if ( not encrtped and self.game_cfg.crypto.encrypted_only + and version >= 120 ): self.logger.error( f"Unencrypted v{version} {endpoint} request, but config is set to encrypted only: {req_raw}" @@ -251,7 +252,7 @@ class OngekiServlet(BaseServlet): zipped = zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) - if not encrtped: + if not encrtped and version < 120: return zipped padded = pad(zipped, 16) From 9537331d1c77410fb00073d03a0dfaf8f876e89f Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Thu, 9 Nov 2023 11:32:04 -0500 Subject: [PATCH 022/101] sao: fix endpoint --- titles/sao/index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/sao/index.py b/titles/sao/index.py index f5c92b7..9d38c14 100644 --- a/titles/sao/index.py +++ b/titles/sao/index.py @@ -51,7 +51,7 @@ class SaoServlet(BaseServlet): def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: return ( [], - [("render_POST", "/SaoServlet/{datecode}/proto/if/{endpoint}", {})] + [("render_POST", "/dev/proto/if/{category}/{endpoint}", {})] ) @classmethod From 580a9faae8d6408253ea87b420b6358a5235f793 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Thu, 9 Nov 2023 11:51:50 -0500 Subject: [PATCH 023/101] sao: fix allnet --- titles/sao/config.py | 23 +++++++++++++++++++++++ titles/sao/index.py | 5 +++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/titles/sao/config.py b/titles/sao/config.py index 7b9a2d5..aa0adbd 100644 --- a/titles/sao/config.py +++ b/titles/sao/config.py @@ -41,7 +41,30 @@ class SaoServerConfig: self.__config, "sao", "server", "auto_register", default=True ) +class SaoCryptConfig: + def __init__(self, parent_config: "SaoConfig"): + self.__config = parent_config + + @property + def enable(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "sao", "crypt", "enable", default=False + ) + + @property + def key(self) -> str: + return CoreConfig.get_config_field( + self.__config, "sao", "crypt", "key", default="" + ) + + @property + def iv(self) -> str: + return CoreConfig.get_config_field( + self.__config, "sao", "crypt", "iv", default="" + ) + class SaoConfig(dict): def __init__(self) -> None: self.server = SaoServerConfig(self) + self.crypt = SaoCryptConfig(self) diff --git a/titles/sao/index.py b/titles/sao/index.py index 9d38c14..177f668 100644 --- a/titles/sao/index.py +++ b/titles/sao/index.py @@ -4,6 +4,7 @@ import yaml import logging, coloredlogs from logging.handlers import TimedRotatingFileHandler from os import path +from Crypto.Cipher import Blowfish from core import CoreConfig, Utils from core.title import BaseServlet @@ -70,8 +71,8 @@ class SaoServlet(BaseServlet): def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]: return ( - f"http://{self.game_cfg.server.hostname}:{self.game_cfg.server.port}/SaoServlet/", - f"{self.game_cfg.server.hostname}/SaoServlet/", + f"http://{self.game_cfg.server.hostname}:{self.game_cfg.server.port}/", + f"{self.game_cfg.server.hostname}/", ) def get_mucha_info(self, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str]: From 8560c05928e40a71acdd334d6ff767421a98cd0a Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Thu, 9 Nov 2023 11:55:49 -0500 Subject: [PATCH 024/101] sao: remove hostname/port config, fix allnet uri --- example_config/sao.yaml | 9 ++++++--- titles/sao/config.py | 12 ------------ titles/sao/index.py | 7 ++++--- 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/example_config/sao.yaml b/example_config/sao.yaml index 7e3ecf2..bfb1c08 100644 --- a/example_config/sao.yaml +++ b/example_config/sao.yaml @@ -1,6 +1,9 @@ server: - hostname: "localhost" enable: True loglevel: "info" - port: 9000 - auto_register: True \ No newline at end of file + auto_register: True + +crypt: + enable: False + key: "" + iv: "" diff --git a/titles/sao/config.py b/titles/sao/config.py index aa0adbd..1ef9991 100644 --- a/titles/sao/config.py +++ b/titles/sao/config.py @@ -5,12 +5,6 @@ class SaoServerConfig: def __init__(self, parent_config: "SaoConfig"): self.__config = parent_config - @property - def hostname(self) -> str: - return CoreConfig.get_config_field( - self.__config, "sao", "server", "hostname", default="localhost" - ) - @property def enable(self) -> bool: return CoreConfig.get_config_field( @@ -25,12 +19,6 @@ class SaoServerConfig: ) ) - @property - def port(self) -> int: - return CoreConfig.get_config_field( - self.__config, "sao", "server", "port", default=9000 - ) - @property def auto_register(self) -> bool: """ diff --git a/titles/sao/index.py b/titles/sao/index.py index 177f668..daa9b32 100644 --- a/titles/sao/index.py +++ b/titles/sao/index.py @@ -52,7 +52,7 @@ class SaoServlet(BaseServlet): def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: return ( [], - [("render_POST", "/dev/proto/if/{category}/{endpoint}", {})] + [("render_POST", "/{datecode}/proto/if/{category}/{endpoint}", {})] ) @classmethod @@ -70,9 +70,10 @@ class SaoServlet(BaseServlet): return True def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]: + tport = Utils.get_title_port(self.core_cfg) return ( - f"http://{self.game_cfg.server.hostname}:{self.game_cfg.server.port}/", - f"{self.game_cfg.server.hostname}/", + f"http://{self.core_cfg.title.hostname}:{tport}/", + f"{self.core_cfg.title.hostname}/", ) def get_mucha_info(self, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str]: From b0ebcaf0cfaaddd47ba62c01eba7e13fa42b20aa Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Thu, 9 Nov 2023 13:16:32 -0500 Subject: [PATCH 025/101] sao: parse header. add crypt --- titles/sao/handlers/base.py | 162 +++++++++++++++++++----------------- titles/sao/index.py | 23 +++-- 2 files changed, 103 insertions(+), 82 deletions(-) diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py index ebcc56e..369b80c 100644 --- a/titles/sao/handlers/base.py +++ b/titles/sao/handlers/base.py @@ -5,10 +5,18 @@ import sys import csv from csv import * -class SaoBaseRequest: +class SaoRequestHeader: def __init__(self, data: bytes) -> None: - self.cmd = struct.unpack_from("!H", bytes)[0] - # TODO: The rest of the request header + collection = struct.unpack_from("!HIIIs16", data) + self.cmd: int = collection[0] + self.vendor_id: int = collection[1] + self.game_id: int = collection[2] + self.version_id: int = collection[3] + self.checksum: str = collection[4] + +class SaoBaseRequest: + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + self.header = header class SaoBaseResponse: def __init__(self, cmd_id: int) -> None: @@ -33,8 +41,8 @@ class SaoNoopResponse(SaoBaseResponse): return super().make() + struct.pack("!bI", self.result, 0) class SaoGetMaintRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) # TODO: The rest of the mait info request class SaoGetMaintResponse(SaoBaseResponse): @@ -68,8 +76,8 @@ class SaoGetMaintResponse(SaoBaseResponse): return super().make() + resp_data class SaoCommonAcCabinetBootNotificationRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoCommonAcCabinetBootNotificationResponse(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -90,8 +98,8 @@ class SaoCommonAcCabinetBootNotificationResponse(SaoBaseResponse): return super().make() + resp_data class SaoMasterDataVersionCheckRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoMasterDataVersionCheckResponse(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -118,8 +126,8 @@ class SaoMasterDataVersionCheckResponse(SaoBaseResponse): return super().make() + resp_data class SaoCommonGetAppVersionsRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoCommonGetAppVersionsRequest(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -154,8 +162,8 @@ class SaoCommonGetAppVersionsRequest(SaoBaseResponse): return super().make() + resp_data class SaoCommonPayingPlayStartRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoCommonPayingPlayStartRequest(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -181,8 +189,8 @@ class SaoCommonPayingPlayStartRequest(SaoBaseResponse): return super().make() + resp_data class SaoGetAuthCardDataRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoGetAuthCardDataResponse(SaoBaseResponse): #GssSite.dll / GssSiteSystem / GameConnectProt / public class get_auth_card_data_R : GameConnect.GssProtocolBase def __init__(self, cmd, profile_data) -> None: @@ -225,8 +233,8 @@ class SaoGetAuthCardDataResponse(SaoBaseResponse): #GssSite.dll / GssSiteSystem return super().make() + resp_data class SaoHomeCheckAcLoginBonusRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoHomeCheckAcLoginBonusResponse(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -262,8 +270,8 @@ class SaoHomeCheckAcLoginBonusResponse(SaoBaseResponse): return super().make() + resp_data class SaoGetQuestSceneMultiPlayPhotonServerRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoGetQuestSceneMultiPlayPhotonServerResponse(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -289,8 +297,8 @@ class SaoGetQuestSceneMultiPlayPhotonServerResponse(SaoBaseResponse): return super().make() + resp_data class SaoTicketRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoTicketResponse(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -318,8 +326,8 @@ class SaoTicketResponse(SaoBaseResponse): return super().make() + resp_data class SaoCommonLoginRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoCommonLoginResponse(SaoBaseResponse): def __init__(self, cmd, profile_data) -> None: @@ -362,8 +370,8 @@ class SaoCommonLoginResponse(SaoBaseResponse): return super().make() + resp_data class SaoCheckComebackEventRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoCheckComebackEventRequest(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -392,8 +400,8 @@ class SaoCheckComebackEventRequest(SaoBaseResponse): return super().make() + resp_data class SaoGetUserBasicDataRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoGetUserBasicDataResponse(SaoBaseResponse): def __init__(self, cmd, profile_data) -> None: @@ -476,8 +484,8 @@ class SaoGetUserBasicDataResponse(SaoBaseResponse): return super().make() + resp_data class SaoGetHeroLogUserDataListRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoGetHeroLogUserDataListResponse(SaoBaseResponse): def __init__(self, cmd, hero_data) -> None: @@ -654,8 +662,8 @@ class SaoGetHeroLogUserDataListResponse(SaoBaseResponse): return super().make() + resp_data class SaoGetEquipmentUserDataListRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoGetEquipmentUserDataListResponse(SaoBaseResponse): def __init__(self, cmd, equipment_data) -> None: @@ -810,8 +818,8 @@ class SaoGetEquipmentUserDataListResponse(SaoBaseResponse): return super().make() + resp_data class SaoGetItemUserDataListRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoGetItemUserDataListResponse(SaoBaseResponse): def __init__(self, cmd, item_data) -> None: @@ -874,8 +882,8 @@ class SaoGetItemUserDataListResponse(SaoBaseResponse): return super().make() + resp_data class SaoGetSupportLogUserDataListRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoGetSupportLogUserDataListResponse(SaoBaseResponse): def __init__(self, cmd, supportIdsData) -> None: @@ -945,8 +953,8 @@ class SaoGetSupportLogUserDataListResponse(SaoBaseResponse): return super().make() + resp_data class SaoGetTitleUserDataListRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoGetTitleUserDataListResponse(SaoBaseResponse): def __init__(self, cmd, titleIdsData) -> None: @@ -993,8 +1001,8 @@ class SaoGetTitleUserDataListResponse(SaoBaseResponse): return super().make() + resp_data class SaoGetEpisodeAppendDataListRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoGetEpisodeAppendDataListResponse(SaoBaseResponse): def __init__(self, cmd, profile_data) -> None: @@ -1051,8 +1059,8 @@ class SaoGetEpisodeAppendDataListResponse(SaoBaseResponse): return super().make() + resp_data class SaoGetPartyDataListRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoGetPartyDataListResponse(SaoBaseResponse): # Default party def __init__(self, cmd, hero1_data, hero2_data, hero3_data) -> None: @@ -1215,8 +1223,8 @@ class SaoGetPartyDataListResponse(SaoBaseResponse): # Default party return super().make() + resp_data class SaoGetQuestScenePrevScanProfileCardRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoGetQuestScenePrevScanProfileCardResponse(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -1352,8 +1360,8 @@ class SaoGetQuestScenePrevScanProfileCardResponse(SaoBaseResponse): return super().make() + resp_data class SaoGetResourcePathInfoRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoGetResourcePathInfoResponse(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -1394,8 +1402,8 @@ class SaoGetResourcePathInfoResponse(SaoBaseResponse): return super().make() + resp_data class SaoEpisodePlayStartRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoEpisodePlayStartResponse(SaoBaseResponse): def __init__(self, cmd, profile_data) -> None: @@ -1441,8 +1449,8 @@ class SaoEpisodePlayStartResponse(SaoBaseResponse): return super().make() + resp_data class SaoEpisodePlayEndRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoEpisodePlayEndResponse(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -1529,8 +1537,8 @@ class SaoEpisodePlayEndResponse(SaoBaseResponse): return super().make() + resp_data class SaoTrialTowerPlayEndRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoTrialTowerPlayEndResponse(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -1655,8 +1663,8 @@ class SaoTrialTowerPlayEndResponse(SaoBaseResponse): return super().make() + resp_data class SaoEpisodePlayEndUnanalyzedLogFixedRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoEpisodePlayEndUnanalyzedLogFixedResponse(SaoBaseResponse): def __init__(self, cmd, end_session_data) -> None: @@ -1735,8 +1743,8 @@ class SaoEpisodePlayEndUnanalyzedLogFixedResponse(SaoBaseResponse): return super().make() + resp_data class SaoGetQuestSceneUserDataListRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoGetQuestSceneUserDataListResponse(SaoBaseResponse): def __init__(self, cmd, quest_data) -> None: @@ -1844,8 +1852,8 @@ class SaoGetQuestSceneUserDataListResponse(SaoBaseResponse): return super().make() + resp_data class SaoCheckYuiMedalGetConditionRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoCheckYuiMedalGetConditionResponse(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -1875,8 +1883,8 @@ class SaoCheckYuiMedalGetConditionResponse(SaoBaseResponse): return super().make() + resp_data class SaoGetYuiMedalBonusUserDataRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoGetYuiMedalBonusUserDataResponse(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -1920,8 +1928,8 @@ class SaoGetYuiMedalBonusUserDataResponse(SaoBaseResponse): return super().make() + resp_data class SaoCheckProfileCardUsedRewardRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoCheckProfileCardUsedRewardResponse(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -1952,8 +1960,8 @@ class SaoCheckProfileCardUsedRewardResponse(SaoBaseResponse): return super().make() + resp_data class SaoSynthesizeEnhancementHeroLogRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoSynthesizeEnhancementHeroLogResponse(SaoBaseResponse): def __init__(self, cmd, hero_data) -> None: @@ -2107,8 +2115,8 @@ class SaoSynthesizeEnhancementHeroLogResponse(SaoBaseResponse): return super().make() + resp_data class SaoSynthesizeEnhancementEquipment(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoSynthesizeEnhancementEquipmentResponse(SaoBaseResponse): def __init__(self, cmd, synthesize_equipment_data) -> None: @@ -2244,8 +2252,8 @@ class SaoSynthesizeEnhancementEquipmentResponse(SaoBaseResponse): return super().make() + resp_data class SaoGetDefragMatchBasicDataRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoGetDefragMatchBasicDataResponse(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -2291,8 +2299,8 @@ class SaoGetDefragMatchBasicDataResponse(SaoBaseResponse): return super().make() + resp_data class SaoGetDefragMatchRankingUserDataRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoGetDefragMatchRankingUserDataResponse(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -2351,8 +2359,8 @@ class SaoGetDefragMatchRankingUserDataResponse(SaoBaseResponse): return super().make() + resp_data class SaoGetDefragMatchLeaguePointRankingListRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoGetDefragMatchLeaguePointRankingListResponse(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -2423,8 +2431,8 @@ class SaoGetDefragMatchLeaguePointRankingListResponse(SaoBaseResponse): return super().make() + resp_data class SaoGetDefragMatchLeagueScoreRankingListRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoGetDefragMatchLeagueScoreRankingListResponse(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -2495,8 +2503,8 @@ class SaoGetDefragMatchLeagueScoreRankingListResponse(SaoBaseResponse): return super().make() + resp_data class SaoBnidSerialCodeCheckRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoBnidSerialCodeCheckResponse(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -2526,8 +2534,8 @@ class SaoBnidSerialCodeCheckResponse(SaoBaseResponse): return super().make() + resp_data class SaoScanQrQuestProfileCardRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoScanQrQuestProfileCardResponse(SaoBaseResponse): def __init__(self, cmd) -> None: diff --git a/titles/sao/index.py b/titles/sao/index.py index daa9b32..509599e 100644 --- a/titles/sao/index.py +++ b/titles/sao/index.py @@ -87,17 +87,30 @@ class SaoServlet(BaseServlet): endpoint = matchers.get('endpoint', '') request.responseHeaders.addRawHeader(b"content-type", b"text/html; charset=utf-8") - sao_request = request.content.getvalue().hex() + req_raw = request.content.read() + sao_request = req_raw.hex() + req_header = SaoRequestHeader(req_raw) + + cmd_str = f"{req_header.cmd:04x}" + + if self.game_cfg.crypt.enable: + iv = req_raw[30:38] + cipher = Blowfish.new(self.game_cfg.crypt.key, Blowfish.MODE_CBC, iv) + crypt_data = req_raw[38:] + req_data = cipher.decrypt(crypt_data) + + else: + req_data = req_raw[30:] - handler = getattr(self.base, f"handle_{sao_request[:4]}", None) + handler = getattr(self.base, f"handle_{cmd_str}", None) if handler is None: - self.logger.info(f"Generic Handler for {endpoint} - {sao_request[:4]}") + self.logger.info(f"Generic Handler for {endpoint} - {cmd_str}") self.logger.debug(f"Request: {request.content.getvalue().hex()}") - resp = SaoNoopResponse(int.from_bytes(bytes.fromhex(sao_request[:4]), "big")+1) + resp = SaoNoopResponse(req_header.cmd + 1) self.logger.debug(f"Response: {resp.make().hex()}") return resp.make() - self.logger.info(f"Handler {endpoint} - {sao_request[:4]} request") + self.logger.info(f"Handler {endpoint} - {cmd_str} request") self.logger.debug(f"Request: {request.content.getvalue().hex()}") resp = handler(sao_request) self.logger.debug(f"Response: {resp.hex()}") From c425ca1ea8e0a0d67600110cc599eeee3049d3a6 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Thu, 9 Nov 2023 13:41:56 -0500 Subject: [PATCH 026/101] sao: hotfix --- titles/sao/handlers/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py index 369b80c..4a3fb3c 100644 --- a/titles/sao/handlers/base.py +++ b/titles/sao/handlers/base.py @@ -7,7 +7,7 @@ from csv import * class SaoRequestHeader: def __init__(self, data: bytes) -> None: - collection = struct.unpack_from("!HIIIs16", data) + collection = struct.unpack_from("!HIII16s", data) self.cmd: int = collection[0] self.vendor_id: int = collection[1] self.game_id: int = collection[2] From 95234a421c07c930fcdf47ff7aecc3178bf4e7fc Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Thu, 9 Nov 2023 13:47:50 -0500 Subject: [PATCH 027/101] sao: fix header --- titles/sao/handlers/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py index 4a3fb3c..08b7937 100644 --- a/titles/sao/handlers/base.py +++ b/titles/sao/handlers/base.py @@ -7,7 +7,7 @@ from csv import * class SaoRequestHeader: def __init__(self, data: bytes) -> None: - collection = struct.unpack_from("!HIII16s", data) + collection = struct.unpack_from("!H6xIII16s", data) self.cmd: int = collection[0] self.vendor_id: int = collection[1] self.game_id: int = collection[2] From 3c06f46644c4749d84459f88b7679f9a8ba5361a Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Thu, 9 Nov 2023 18:11:58 -0500 Subject: [PATCH 028/101] sao: add decoder helpers, hash checks --- example_config/sao.yaml | 4 +++ titles/sao/config.py | 17 +++++++++++ titles/sao/handlers/__init__.py | 3 +- titles/sao/handlers/base.py | 34 +++++++++++++++++++--- titles/sao/handlers/helpers.py | 50 +++++++++++++++++++++++++++++++++ titles/sao/index.py | 17 +++++++++-- 6 files changed, 117 insertions(+), 8 deletions(-) create mode 100644 titles/sao/handlers/helpers.py diff --git a/example_config/sao.yaml b/example_config/sao.yaml index bfb1c08..0209ffe 100644 --- a/example_config/sao.yaml +++ b/example_config/sao.yaml @@ -7,3 +7,7 @@ crypt: enable: False key: "" iv: "" + +hash: + verify_hash: False + hash_base: "" \ No newline at end of file diff --git a/titles/sao/config.py b/titles/sao/config.py index 1ef9991..33d319b 100644 --- a/titles/sao/config.py +++ b/titles/sao/config.py @@ -51,8 +51,25 @@ class SaoCryptConfig: self.__config, "sao", "crypt", "iv", default="" ) +class SaoHashConfig: + def __init__(self, parent_config: "SaoConfig"): + self.__config = parent_config + + @property + def verify_hash(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "sao", "hash", "verify_hash", default=False + ) + + @property + def hash_base(self) -> str: + return CoreConfig.get_config_field( + self.__config, "sao", "hash", "hash_base", default="" + ) + class SaoConfig(dict): def __init__(self) -> None: self.server = SaoServerConfig(self) self.crypt = SaoCryptConfig(self) + self.hash = SaoHashConfig(self) diff --git a/titles/sao/handlers/__init__.py b/titles/sao/handlers/__init__.py index 90a6b4e..4d38136 100644 --- a/titles/sao/handlers/__init__.py +++ b/titles/sao/handlers/__init__.py @@ -1 +1,2 @@ -from titles.sao.handlers.base import * \ No newline at end of file +from .base import * +from .helpers import * \ No newline at end of file diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py index 08b7937..579e2c8 100644 --- a/titles/sao/handlers/base.py +++ b/titles/sao/handlers/base.py @@ -1,22 +1,26 @@ import struct from datetime import datetime from construct import * -import sys +from .helpers import * import csv from csv import * class SaoRequestHeader: def __init__(self, data: bytes) -> None: - collection = struct.unpack_from("!H6xIII16s", data) + collection = struct.unpack_from("!H6xIII16sI", data) self.cmd: int = collection[0] self.vendor_id: int = collection[1] self.game_id: int = collection[2] self.version_id: int = collection[3] - self.checksum: str = collection[4] + self.hash: str = collection[4] + self.data_len: str = collection[5] class SaoBaseRequest: def __init__(self, header: SaoRequestHeader, data: bytes) -> None: self.header = header + if self.header.data_len != len(data): + logging.getLogger('sao').error(f"Expected {self.header.data_len} data bytes byt got {len(data)}!") + # TODO: Raise an error here class SaoBaseResponse: def __init__(self, cmd_id: int) -> None: @@ -2749,4 +2753,26 @@ class SaoScanQrQuestProfileCardResponse(SaoBaseResponse): resp_data = resp_struct.build(resp_data) self.length = len(resp_data) - return super().make() + resp_data \ No newline at end of file + return super().make() + resp_data + +class SaoConsumeCreditGuestRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + shop_id = decode_str(data, off) + self.shop_id = shop_id[0] + off += shop_id[1] + + serial_num = decode_str(data, off) + self.serial_num = serial_num[0] + off += serial_num[1] + + self.cab_type = decode_byte(data, off) + off += BYTE_OFF + + self.act_type = decode_byte(data, off) + off += BYTE_OFF + + self.consume_num = decode_byte(data, off) + off += BYTE_OFF + \ No newline at end of file diff --git a/titles/sao/handlers/helpers.py b/titles/sao/handlers/helpers.py new file mode 100644 index 0000000..c4d4214 --- /dev/null +++ b/titles/sao/handlers/helpers.py @@ -0,0 +1,50 @@ +from typing import Tuple +import struct +import logging + +BIGINT_OFF = 16 +LONG_OFF = 8 +INT_OFF = 4 +SHORT_OFF = 2 +BYTE_OFF = 1 + +def decode_num(data: bytes, offset: int, size: int) -> int: + try: + return int.from_bytes(data[offset:offset + size], 'big') + except: + logging.getLogger('sao').error(f"Failed to parse {data[offset:offset + size]} as BE number of width {size}") + return 0 + +def decode_byte(data: bytes, offset: int) -> int: + return decode_num(data, offset, BYTE_OFF) + +def decode_short(data: bytes, offset: int) -> int: + return decode_num(data, offset, SHORT_OFF) + +def decode_int(data: bytes, offset: int) -> int: + return decode_num(data, offset, INT_OFF) + +def decode_long(data: bytes, offset: int) -> int: + return decode_num(data, offset, LONG_OFF) + +def decode_bigint(data: bytes, offset: int) -> int: + return decode_num(data, offset, BIGINT_OFF) + +def decode_str(data: bytes, offset: int) -> Tuple[str, int]: + try: + str_len = decode_int(data, offset) + num_bytes_decoded = INT_OFF + str_len + str_out = data[offset + INT_OFF:offset + num_bytes_decoded].decode("utf-16-le", errors="replace") + return (str_out, num_bytes_decoded) + except: + logging.getLogger('sao').error(f"Failed to parse {data[offset:]} as string!") + return ("", 0) + +def encode_str(s: str) -> bytes: + try: + str_bytes = s.encode("utf-16-le", errors="replace") + str_len_bytes = struct.pack("!I", len(str_bytes)) + return str_len_bytes + str_bytes + except: + logging.getLogger('sao').error(f"Failed to encode {s} as bytes!") + return b"" \ No newline at end of file diff --git a/titles/sao/index.py b/titles/sao/index.py index 509599e..7f49e64 100644 --- a/titles/sao/index.py +++ b/titles/sao/index.py @@ -5,6 +5,7 @@ import logging, coloredlogs from logging.handlers import TimedRotatingFileHandler from os import path from Crypto.Cipher import Blowfish +from hashlib import md5 from core import CoreConfig, Utils from core.title import BaseServlet @@ -48,6 +49,10 @@ class SaoServlet(BaseServlet): self.logger.inited = True self.base = SaoBase(core_cfg, self.game_cfg) + self.static_hash = None + + if self.game_cfg.hash.verify_hash: + self.static_hash = md5(self.game_cfg.hash.hash_base) # Greate hashing guys, really validates the data def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: return ( @@ -93,14 +98,20 @@ class SaoServlet(BaseServlet): cmd_str = f"{req_header.cmd:04x}" + if self.game_cfg.hash.verify_hash and self.static_hash != req_header.hash: + self.logger.error(f"Hash mismatch! Expecting {self.static_hash} but recieved {req_header.hash}") + return b"" + if self.game_cfg.crypt.enable: - iv = req_raw[30:38] + + iv = req_raw[40:48] cipher = Blowfish.new(self.game_cfg.crypt.key, Blowfish.MODE_CBC, iv) - crypt_data = req_raw[38:] + crypt_data = req_raw[48:] req_data = cipher.decrypt(crypt_data) + self.logger.debug(f"Decrypted {req_data.hex()} with IV {iv.hex()}") else: - req_data = req_raw[30:] + req_data = req_raw[40:] handler = getattr(self.base, f"handle_{cmd_str}", None) if handler is None: From 8769c99f4a6e154a93990462ae071f78a77a8229 Mon Sep 17 00:00:00 2001 From: Midorica Date: Thu, 9 Nov 2023 19:34:04 -0500 Subject: [PATCH 029/101] fixing ongeki handler --- titles/ongeki/index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/ongeki/index.py b/titles/ongeki/index.py index 26686a2..9cc8891 100644 --- a/titles/ongeki/index.py +++ b/titles/ongeki/index.py @@ -252,7 +252,7 @@ class OngekiServlet(BaseServlet): zipped = zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) - if not encrtped and version < 120: + if not encrtped or version < 120: return zipped padded = pad(zipped, 16) From 840db275fab55056f24272a2bd2e35d1fbd2f9e7 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Thu, 9 Nov 2023 20:56:03 -0500 Subject: [PATCH 030/101] sao: add encrtped response --- titles/sao/index.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/titles/sao/index.py b/titles/sao/index.py index 7f49e64..1a55885 100644 --- a/titles/sao/index.py +++ b/titles/sao/index.py @@ -6,6 +6,7 @@ from logging.handlers import TimedRotatingFileHandler from os import path from Crypto.Cipher import Blowfish from hashlib import md5 +import random from core import CoreConfig, Utils from core.title import BaseServlet @@ -87,10 +88,10 @@ class SaoServlet(BaseServlet): return (True, "SAO1") - def render_POST(self, request: Request, game_code: str, matchers: Dict) -> bytes: endpoint = matchers.get('endpoint', '') request.responseHeaders.addRawHeader(b"content-type", b"text/html; charset=utf-8") + iv = b"" req_raw = request.content.read() sao_request = req_raw.hex() @@ -103,7 +104,6 @@ class SaoServlet(BaseServlet): return b"" if self.game_cfg.crypt.enable: - iv = req_raw[40:48] cipher = Blowfish.new(self.game_cfg.crypt.key, Blowfish.MODE_CBC, iv) crypt_data = req_raw[48:] @@ -125,4 +125,11 @@ class SaoServlet(BaseServlet): self.logger.debug(f"Request: {request.content.getvalue().hex()}") resp = handler(sao_request) self.logger.debug(f"Response: {resp.hex()}") + + if self.game_cfg.crypt.enable: + iv = random.randbytes(8) + cipher = Blowfish.new(self.game_cfg.crypt.key, Blowfish.MODE_CBC, iv) + data_crypt = cipher.encrypt(resp[24:]) + resp = resp[:24] + iv + data_crypt + return resp \ No newline at end of file From e6801c1c460dd206465a7853ad4bf1f0c7817c1f Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Thu, 9 Nov 2023 20:56:33 -0500 Subject: [PATCH 031/101] sao: add logging --- titles/sao/index.py | 1 + 1 file changed, 1 insertion(+) diff --git a/titles/sao/index.py b/titles/sao/index.py index 1a55885..28da89a 100644 --- a/titles/sao/index.py +++ b/titles/sao/index.py @@ -131,5 +131,6 @@ class SaoServlet(BaseServlet): cipher = Blowfish.new(self.game_cfg.crypt.key, Blowfish.MODE_CBC, iv) data_crypt = cipher.encrypt(resp[24:]) resp = resp[:24] + iv + data_crypt + self.logger.debug(f"Encrypted Response: {resp.hex()}") return resp \ No newline at end of file From 81d588cbc751e65e10a8677480a2d6f5a117acf5 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Thu, 9 Nov 2023 21:00:51 -0500 Subject: [PATCH 032/101] sao: crypt fixes --- titles/sao/index.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/titles/sao/index.py b/titles/sao/index.py index 28da89a..1fddc0b 100644 --- a/titles/sao/index.py +++ b/titles/sao/index.py @@ -53,7 +53,7 @@ class SaoServlet(BaseServlet): self.static_hash = None if self.game_cfg.hash.verify_hash: - self.static_hash = md5(self.game_cfg.hash.hash_base) # Greate hashing guys, really validates the data + self.static_hash = md5(self.game_cfg.hash.hash_base.encode()).digest() # Greate hashing guys, really validates the data def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: return ( @@ -105,7 +105,7 @@ class SaoServlet(BaseServlet): if self.game_cfg.crypt.enable: iv = req_raw[40:48] - cipher = Blowfish.new(self.game_cfg.crypt.key, Blowfish.MODE_CBC, iv) + cipher = Blowfish.new(self.game_cfg.crypt.key.encode(), Blowfish.MODE_CBC, iv) crypt_data = req_raw[48:] req_data = cipher.decrypt(crypt_data) self.logger.debug(f"Decrypted {req_data.hex()} with IV {iv.hex()}") @@ -128,7 +128,7 @@ class SaoServlet(BaseServlet): if self.game_cfg.crypt.enable: iv = random.randbytes(8) - cipher = Blowfish.new(self.game_cfg.crypt.key, Blowfish.MODE_CBC, iv) + cipher = Blowfish.new(self.game_cfg.crypt.key.encode(), Blowfish.MODE_CBC, iv) data_crypt = cipher.encrypt(resp[24:]) resp = resp[:24] + iv + data_crypt self.logger.debug(f"Encrypted Response: {resp.hex()}") From 8d190ce7f0f9525b17c5ffbb0e73fd17d2d2af03 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Thu, 9 Nov 2023 21:05:30 -0500 Subject: [PATCH 033/101] sao: fix padding --- titles/sao/index.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/titles/sao/index.py b/titles/sao/index.py index 1fddc0b..ce5b826 100644 --- a/titles/sao/index.py +++ b/titles/sao/index.py @@ -128,8 +128,12 @@ class SaoServlet(BaseServlet): if self.game_cfg.crypt.enable: iv = random.randbytes(8) + data_to_crypt = resp[24:] + while len(data_to_crypt) % 8 != 0: + data_to_crypt += b"\x00" + cipher = Blowfish.new(self.game_cfg.crypt.key.encode(), Blowfish.MODE_CBC, iv) - data_crypt = cipher.encrypt(resp[24:]) + data_crypt = cipher.encrypt(data_to_crypt) resp = resp[:24] + iv + data_crypt self.logger.debug(f"Encrypted Response: {resp.hex()}") From 4cd1181ef7a03fbd2820829d8e77e5b93fcebeea Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Thu, 9 Nov 2023 21:09:55 -0500 Subject: [PATCH 034/101] sao: fix generic handler --- titles/sao/index.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/titles/sao/index.py b/titles/sao/index.py index ce5b826..84256fa 100644 --- a/titles/sao/index.py +++ b/titles/sao/index.py @@ -116,14 +116,15 @@ class SaoServlet(BaseServlet): handler = getattr(self.base, f"handle_{cmd_str}", None) if handler is None: self.logger.info(f"Generic Handler for {endpoint} - {cmd_str}") - self.logger.debug(f"Request: {request.content.getvalue().hex()}") - resp = SaoNoopResponse(req_header.cmd + 1) - self.logger.debug(f"Response: {resp.make().hex()}") - return resp.make() - - self.logger.info(f"Handler {endpoint} - {cmd_str} request") - self.logger.debug(f"Request: {request.content.getvalue().hex()}") - resp = handler(sao_request) + self.logger.debug(f"Request: {req_raw.hex()}") + resp_thing = SaoNoopResponse(req_header.cmd + 1) + resp = resp_thing.make() + + else: + self.logger.info(f"Handler {endpoint} - {cmd_str} request") + self.logger.debug(f"Request: {req_raw.hex()}") + resp = handler(sao_request) + self.logger.debug(f"Response: {resp.hex()}") if self.game_cfg.crypt.enable: From 0fa8fe06f0fde03be22b0d3ca86f920f997799ca Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Thu, 9 Nov 2023 21:17:18 -0500 Subject: [PATCH 035/101] sao: fix crypt maybe --- titles/sao/index.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/titles/sao/index.py b/titles/sao/index.py index 84256fa..9d40e21 100644 --- a/titles/sao/index.py +++ b/titles/sao/index.py @@ -135,7 +135,9 @@ class SaoServlet(BaseServlet): cipher = Blowfish.new(self.game_cfg.crypt.key.encode(), Blowfish.MODE_CBC, iv) data_crypt = cipher.encrypt(data_to_crypt) - resp = resp[:24] + iv + data_crypt + crypt_data_len = len(data_crypt) + tmp = struct.pack("!I", crypt_data_len) # does it want the length of the encrypted response?? + resp = resp[:20] + tmp + iv + data_crypt self.logger.debug(f"Encrypted Response: {resp.hex()}") return resp \ No newline at end of file From 18bf250fd739146a12fb7bd971eb4f262ce3afbb Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Thu, 9 Nov 2023 21:23:19 -0500 Subject: [PATCH 036/101] sao: fix length calculation --- titles/sao/index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/sao/index.py b/titles/sao/index.py index 9d40e21..24a7188 100644 --- a/titles/sao/index.py +++ b/titles/sao/index.py @@ -135,7 +135,7 @@ class SaoServlet(BaseServlet): cipher = Blowfish.new(self.game_cfg.crypt.key.encode(), Blowfish.MODE_CBC, iv) data_crypt = cipher.encrypt(data_to_crypt) - crypt_data_len = len(data_crypt) + crypt_data_len = len(data_crypt) + len(iv) tmp = struct.pack("!I", crypt_data_len) # does it want the length of the encrypted response?? resp = resp[:20] + tmp + iv + data_crypt self.logger.debug(f"Encrypted Response: {resp.hex()}") From 3d62b9d3728d5d1fac4b49306ff57c1cde0ac7b1 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Thu, 9 Nov 2023 22:36:33 -0500 Subject: [PATCH 037/101] sao: add SaoResponseHeader --- titles/sao/handlers/base.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py index 579e2c8..3579a9e 100644 --- a/titles/sao/handlers/base.py +++ b/titles/sao/handlers/base.py @@ -22,7 +22,7 @@ class SaoBaseRequest: logging.getLogger('sao').error(f"Expected {self.header.data_len} data bytes byt got {len(data)}!") # TODO: Raise an error here -class SaoBaseResponse: +class SaoResponseHeader: def __init__(self, cmd_id: int) -> None: self.cmd = cmd_id self.err_status = 0 @@ -35,6 +35,13 @@ class SaoBaseResponse: def make(self) -> bytes: return struct.pack("!HHIIIII", self.cmd, self.err_status, self.error_type, self.vendor_id, self.game_id, self.version_id, self.length) +class SaoBaseResponse: + def __init__(self, cmd_id: int) -> None: + self.header = SaoResponseHeader(cmd_id) + + def make(self) -> bytes: + return self.header.make() + class SaoNoopResponse(SaoBaseResponse): def __init__(self, cmd: int) -> None: super().__init__(cmd) From 44755d4d0f4ca043f44bb81658b3016c0f10fde9 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Thu, 9 Nov 2023 23:29:06 -0500 Subject: [PATCH 038/101] sao: begin implementing request helpers --- titles/sao/base.py | 368 +++++++++++---------------------- titles/sao/handlers/base.py | 87 ++++++++ titles/sao/handlers/helpers.py | 19 +- titles/sao/index.py | 15 +- 4 files changed, 232 insertions(+), 257 deletions(-) diff --git a/titles/sao/base.py b/titles/sao/base.py index 3c4aade..24dd22d 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -22,71 +22,51 @@ class SaoBase: self.version = 0 self.logger = logging.getLogger("sao") - def handle_noop(self, request: Any) -> bytes: - sao_request = request + def handle_noop(self, header: SaoRequestHeader, request: bytes) -> bytes: + self.logger.info(f"Using Generic handler") + resp_thing = SaoNoopResponse(header.cmd + 1) + return resp_thing.make() - sao_id = int(sao_request[:4],16) + 1 - - ret = struct.pack("!HHIIIIIIb", sao_id, 0, 0, 5, 1, 1, 5, 0x01000000, 0).hex() - return bytes.fromhex(ret) - - def handle_c122(self, request: Any) -> bytes: + def handle_c122(self, header: SaoRequestHeader, request: bytes) -> bytes: #common/get_maintenance_info - resp = SaoGetMaintResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoGetMaintResponse(header.cmd +1) return resp.make() - def handle_c12e(self, request: Any) -> bytes: + def handle_c12e(self, header: SaoRequestHeader, request: bytes) -> bytes: #common/ac_cabinet_boot_notification - resp = SaoCommonAcCabinetBootNotificationResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoCommonAcCabinetBootNotificationResponse(header.cmd +1) return resp.make() - def handle_c100(self, request: Any) -> bytes: + def handle_c100(self, header: SaoRequestHeader, request: bytes) -> bytes: #common/get_app_versions - resp = SaoCommonGetAppVersionsRequest(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoCommonGetAppVersionsRequest(header.cmd +1) return resp.make() - def handle_c102(self, request: Any) -> bytes: + def handle_c102(self, header: SaoRequestHeader, request: bytes) -> bytes: #common/master_data_version_check - resp = SaoMasterDataVersionCheckResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoMasterDataVersionCheckResponse(header.cmd +1) return resp.make() - def handle_c10a(self, request: Any) -> bytes: + def handle_c10a(self, header: SaoRequestHeader, request: bytes) -> bytes: #common/paying_play_start - resp = SaoCommonPayingPlayStartRequest(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoCommonPayingPlayStartRequest(header.cmd +1) return resp.make() - def handle_ca02(self, request: Any) -> bytes: + def handle_ca02(self, header: SaoRequestHeader, request: bytes) -> bytes: #quest_multi_play_room/get_quest_scene_multi_play_photon_server - resp = SaoGetQuestSceneMultiPlayPhotonServerResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoGetQuestSceneMultiPlayPhotonServerResponse(header.cmd +1) return resp.make() - def handle_c11e(self, request: Any) -> bytes: + def handle_c11e(self, header: SaoRequestHeader, request: bytes) -> bytes: #common/get_auth_card_data - req = bytes.fromhex(request)[24:] - - req_struct = Struct( - Padding(16), - "cabinet_type" / Int8ub, # cabinet_type is a byte - "auth_type" / Int8ub, # auth_type is a byte - "store_id_size" / Rebuild(Int32ub, len_(this.store_id) * 2), # calculates the length of the store_id - "store_id" / PaddedString(this.store_id_size, "utf_16_le"), # store_id is a (zero) padded string - "serial_no_size" / Rebuild(Int32ub, len_(this.serial_no) * 2), # calculates the length of the serial_no - "serial_no" / PaddedString(this.serial_no_size, "utf_16_le"), # serial_no is a (zero) padded string - "access_code_size" / Rebuild(Int32ub, len_(this.access_code) * 2), # calculates the length of the access_code - "access_code" / PaddedString(this.access_code_size, "utf_16_le"), # access_code is a (zero) padded string - "chip_id_size" / Rebuild(Int32ub, len_(this.chip_id) * 2), # calculates the length of the chip_id - "chip_id" / PaddedString(this.chip_id_size, "utf_16_le"), # chip_id is a (zero) padded string - ) - - req_data = req_struct.parse(req) - access_code = req_data.access_code + req = SaoGetAuthCardDataRequest(header, request) #Check authentication - user_id = self.core_data.card.get_user_id_from_card( access_code ) + user_id = self.core_data.card.get_user_id_from_card( req.access_code ) if not user_id: user_id = self.core_data.user.create_user() #works - card_id = self.core_data.card.create_card(user_id, access_code) + card_id = self.core_data.card.create_card(user_id, req.access_code) if card_id is None: user_id = -1 @@ -106,7 +86,7 @@ class SaoBase: # Force the tutorial stage to be completed due to potential crash in-game - self.logger.info(f"User Authenticated: { access_code } | { user_id }") + self.logger.info(f"User Authenticated: { req.access_code } | { user_id }") #Grab values from profile profile_data = self.game_data.profile.get_profile(user_id) @@ -127,270 +107,168 @@ class SaoBase: profile_data = self.game_data.profile.get_profile(user_id) - resp = SaoGetAuthCardDataResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data) + resp = SaoGetAuthCardDataResponse(header.cmd +1, profile_data) return resp.make() - def handle_c40c(self, request: Any) -> bytes: + def handle_c40c(self, header: SaoRequestHeader, request: bytes) -> bytes: #home/check_ac_login_bonus - resp = SaoHomeCheckAcLoginBonusResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoHomeCheckAcLoginBonusResponse(header.cmd +1) return resp.make() - def handle_c104(self, request: Any) -> bytes: + def handle_c104(self, header: SaoRequestHeader, request: bytes) -> bytes: #common/login - req = bytes.fromhex(request)[24:] + req = SaoCommonLoginRequest(header, request) - req_struct = Struct( - Padding(16), - "cabinet_type" / Int8ub, # cabinet_type is a byte - "auth_type" / Int8ub, # auth_type is a byte - "store_id_size" / Rebuild(Int32ub, len_(this.store_id) * 2), # calculates the length of the store_id - "store_id" / PaddedString(this.store_id_size, "utf_16_le"), # store_id is a (zero) padded string - "store_name_size" / Rebuild(Int32ub, len_(this.store_name) * 2), # calculates the length of the store_name - "store_name" / PaddedString(this.store_name_size, "utf_16_le"), # store_name is a (zero) padded string - "serial_no_size" / Rebuild(Int32ub, len_(this.serial_no) * 2), # calculates the length of the serial_no - "serial_no" / PaddedString(this.serial_no_size, "utf_16_le"), # serial_no is a (zero) padded string - "access_code_size" / Rebuild(Int32ub, len_(this.access_code) * 2), # calculates the length of the access_code - "access_code" / PaddedString(this.access_code_size, "utf_16_le"), # access_code is a (zero) padded string - "chip_id_size" / Rebuild(Int32ub, len_(this.chip_id) * 2), # calculates the length of the chip_id - "chip_id" / PaddedString(this.chip_id_size, "utf_16_le"), # chip_id is a (zero) padded string - "free_ticket_distribution_target_flag" / Int8ub, # free_ticket_distribution_target_flag is a byte - ) - - req_data = req_struct.parse(req) - access_code = req_data.access_code - - user_id = self.core_data.card.get_user_id_from_card( access_code ) + user_id = self.core_data.card.get_user_id_from_card( req.access_code ) profile_data = self.game_data.profile.get_profile(user_id) - resp = SaoCommonLoginResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data) + resp = SaoCommonLoginResponse(header.cmd +1, profile_data) return resp.make() - def handle_c404(self, request: Any) -> bytes: + def handle_c404(self, header: SaoRequestHeader, request: bytes) -> bytes: #home/check_comeback_event - resp = SaoCheckComebackEventRequest(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoCheckComebackEventRequest(header.cmd +1) return resp.make() - def handle_c000(self, request: Any) -> bytes: + def handle_c000(self, header: SaoRequestHeader, request: bytes) -> bytes: #ticket/ticket - resp = SaoTicketResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoTicketResponse(header.cmd +1) return resp.make() - def handle_c500(self, request: Any) -> bytes: + def handle_c500(self, header: SaoRequestHeader, request: bytes) -> bytes: #user_info/get_user_basic_data - req = bytes.fromhex(request)[24:] + req = SaoGetUserBasicDataRequest(header, request) - req_struct = Struct( - Padding(16), - "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id - "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string - ) + profile_data = self.game_data.profile.get_profile(req.user_id) - req_data = req_struct.parse(req) - user_id = req_data.user_id - - profile_data = self.game_data.profile.get_profile(user_id) - - resp = SaoGetUserBasicDataResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data) + resp = SaoGetUserBasicDataResponse(header.cmd +1, profile_data) return resp.make() - def handle_c600(self, request: Any) -> bytes: + def handle_c600(self, header: SaoRequestHeader, request: bytes) -> bytes: #have_object/get_hero_log_user_data_list - req = bytes.fromhex(request)[24:] - - req_struct = Struct( - Padding(16), - "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id - "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string + req = SaoGetHeroLogUserDataListRequest(header, request) - ) - req_data = req_struct.parse(req) - user_id = req_data.user_id - - hero_data = self.game_data.item.get_hero_logs(user_id) + hero_data = self.game_data.item.get_hero_logs(req.user_id) - resp = SaoGetHeroLogUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, hero_data) + resp = SaoGetHeroLogUserDataListResponse(header.cmd +1, hero_data) return resp.make() - def handle_c602(self, request: Any) -> bytes: + def handle_c602(self, header: SaoRequestHeader, request: bytes) -> bytes: #have_object/get_equipment_user_data_list - req = bytes.fromhex(request)[24:] - - req_struct = Struct( - Padding(16), - "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id - "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string - - ) - req_data = req_struct.parse(req) - user_id = req_data.user_id + req = SaoGetEquipmentUserDataListRequest(header, request) - equipment_data = self.game_data.item.get_user_equipments(user_id) + equipment_data = self.game_data.item.get_user_equipments(req.user_id) - resp = SaoGetEquipmentUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, equipment_data) + resp = SaoGetEquipmentUserDataListResponse(header.cmd +1, equipment_data) return resp.make() - def handle_c604(self, request: Any) -> bytes: + def handle_c604(self, header: SaoRequestHeader, request: bytes) -> bytes: #have_object/get_item_user_data_list - req = bytes.fromhex(request)[24:] + req = SaoGetItemUserDataListRequest(header, request) - req_struct = Struct( - Padding(16), - "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id - "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string + item_data = self.game_data.item.get_user_items(req.user_id) - ) - req_data = req_struct.parse(req) - user_id = req_data.user_id - - item_data = self.game_data.item.get_user_items(user_id) - - resp = SaoGetItemUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, item_data) + resp = SaoGetItemUserDataListResponse(header.cmd +1, item_data) return resp.make() - def handle_c606(self, request: Any) -> bytes: + def handle_c606(self, header: SaoRequestHeader, request: bytes) -> bytes: #have_object/get_support_log_user_data_list supportIdsData = self.game_data.static.get_support_log_ids(0, True) - resp = SaoGetSupportLogUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, supportIdsData) + resp = SaoGetSupportLogUserDataListResponse(header.cmd +1, supportIdsData) return resp.make() - def handle_c800(self, request: Any) -> bytes: + def handle_c800(self, header: SaoRequestHeader, request: bytes) -> bytes: #custom/get_title_user_data_list titleIdsData = self.game_data.static.get_title_ids(0, True) - resp = SaoGetTitleUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, titleIdsData) + resp = SaoGetTitleUserDataListResponse(header.cmd +1, titleIdsData) return resp.make() - def handle_c608(self, request: Any) -> bytes: + def handle_c608(self, header: SaoRequestHeader, request: bytes) -> bytes: #have_object/get_episode_append_data_list - req = bytes.fromhex(request)[24:] + req = SaoGetEpisodeAppendDataListRequest(header, request) - req_struct = Struct( - Padding(16), - "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id - "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string - ) + profile_data = self.game_data.profile.get_profile(req.user_id) - req_data = req_struct.parse(req) - user_id = req_data.user_id - - profile_data = self.game_data.profile.get_profile(user_id) - - resp = SaoGetEpisodeAppendDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data) + resp = SaoGetEpisodeAppendDataListResponse(header.cmd +1, profile_data) return resp.make() - def handle_c804(self, request: Any) -> bytes: + def handle_c804(self, header: SaoRequestHeader, request: bytes) -> bytes: #custom/get_party_data_list - req = bytes.fromhex(request)[24:] + req = SaoGetPartyDataListRequest(header, request) - req_struct = Struct( - Padding(16), - "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id - "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string + hero_party = self.game_data.item.get_hero_party(req.user_id, 0) + hero1_data = self.game_data.item.get_hero_log(req.user_id, hero_party[3]) + hero2_data = self.game_data.item.get_hero_log(req.user_id, hero_party[4]) + hero3_data = self.game_data.item.get_hero_log(req.user_id, hero_party[5]) - ) - req_data = req_struct.parse(req) - user_id = req_data.user_id - - hero_party = self.game_data.item.get_hero_party(user_id, 0) - hero1_data = self.game_data.item.get_hero_log(user_id, hero_party[3]) - hero2_data = self.game_data.item.get_hero_log(user_id, hero_party[4]) - hero3_data = self.game_data.item.get_hero_log(user_id, hero_party[5]) - - resp = SaoGetPartyDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, hero1_data, hero2_data, hero3_data) + resp = SaoGetPartyDataListResponse(header.cmd +1, hero1_data, hero2_data, hero3_data) return resp.make() - def handle_c902(self, request: Any) -> bytes: # for whatever reason, having all entries empty or filled changes nothing + def handle_c902(self, header: SaoRequestHeader, request: bytes) -> bytes: # for whatever reason, having all entries empty or filled changes nothing #quest/get_quest_scene_prev_scan_profile_card - resp = SaoGetQuestScenePrevScanProfileCardResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoGetQuestScenePrevScanProfileCardResponse(header.cmd +1) return resp.make() - def handle_c124(self, request: Any) -> bytes: + def handle_c124(self, header: SaoRequestHeader, request: bytes) -> bytes: #common/get_resource_path_info - resp = SaoGetResourcePathInfoResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoGetResourcePathInfoResponse(header.cmd +1) return resp.make() - def handle_c900(self, request: Any) -> bytes: + def handle_c900(self, header: SaoRequestHeader, request: bytes) -> bytes: #quest/get_quest_scene_user_data_list // QuestScene.csv - req = bytes.fromhex(request)[24:] + req = SaoGetQuestSceneUserDataListRequest(header, request) - req_struct = Struct( - Padding(16), - "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id - "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string + quest_data = self.game_data.item.get_quest_logs(req.user_id) - ) - req_data = req_struct.parse(req) - user_id = req_data.user_id - - quest_data = self.game_data.item.get_quest_logs(user_id) - - resp = SaoGetQuestSceneUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, quest_data) + resp = SaoGetQuestSceneUserDataListResponse(header.cmd +1, quest_data) return resp.make() - def handle_c400(self, request: Any) -> bytes: + def handle_c400(self, header: SaoRequestHeader, request: bytes) -> bytes: #home/check_yui_medal_get_condition - resp = SaoCheckYuiMedalGetConditionResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoCheckYuiMedalGetConditionResponse(header.cmd +1) return resp.make() - def handle_c402(self, request: Any) -> bytes: + def handle_c402(self, header: SaoRequestHeader, request: bytes) -> bytes: #home/get_yui_medal_bonus_user_data - resp = SaoGetYuiMedalBonusUserDataResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoGetYuiMedalBonusUserDataResponse(header.cmd +1) return resp.make() - def handle_c40a(self, request: Any) -> bytes: + def handle_c40a(self, header: SaoRequestHeader, request: bytes) -> bytes: #home/check_profile_card_used_reward - resp = SaoCheckProfileCardUsedRewardResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoCheckProfileCardUsedRewardResponse(header.cmd +1) return resp.make() - def handle_c814(self, request: Any) -> bytes: + def handle_c814(self, header: SaoRequestHeader, request: bytes) -> bytes: #custom/synthesize_enhancement_hero_log - req = bytes.fromhex(request)[24:] + req = SaoSynthesizeEnhancementHeroLogRequest(header, request) - req_struct = Struct( - Padding(20), - "ticket_id" / Bytes(1), # needs to be parsed as an int - Padding(1), - "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id - "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string - "origin_user_hero_log_id_size" / Rebuild(Int32ub, len_(this.origin_user_hero_log_id) * 2), # calculates the length of the origin_user_hero_log_id - "origin_user_hero_log_id" / PaddedString(this.origin_user_hero_log_id_size, "utf_16_le"), # origin_user_hero_log_id is a (zero) padded string - Padding(3), - "material_common_reward_user_data_list_length" / Rebuild(Int8ub, len_(this.material_common_reward_user_data_list)), # material_common_reward_user_data_list is a byte, - "material_common_reward_user_data_list" / Array(this.material_common_reward_user_data_list_length, Struct( - "common_reward_type" / Int16ub, # team_no is a byte - "user_common_reward_id_size" / Rebuild(Int32ub, len_(this.user_common_reward_id) * 2), # calculates the length of the user_common_reward_id - "user_common_reward_id" / PaddedString(this.user_common_reward_id_size, "utf_16_le"), # user_common_reward_id is a (zero) padded string - )), - ) + synthesize_hero_log_data = self.game_data.item.get_hero_log(req.user_id, req.origin_user_hero_log_id) - req_data = req_struct.parse(req) - user_id = req_data.user_id - synthesize_hero_log_data = self.game_data.item.get_hero_log(req_data.user_id, req_data.origin_user_hero_log_id) + for i in range(req.material_common_reward_user_data_count): - for i in range(0,req_data.material_common_reward_user_data_list_length): - - itemList = self.game_data.static.get_item_id(req_data.material_common_reward_user_data_list[i].user_common_reward_id) - heroList = self.game_data.static.get_hero_id(req_data.material_common_reward_user_data_list[i].user_common_reward_id) - equipmentList = self.game_data.static.get_equipment_id(req_data.material_common_reward_user_data_list[i].user_common_reward_id) + itemList = self.game_data.static.get_item_id(req.material_common_reward_user_data_list[i].user_common_reward_id) + heroList = self.game_data.static.get_hero_id(req.material_common_reward_user_data_list[i].user_common_reward_id) + equipmentList = self.game_data.static.get_equipment_id(req.material_common_reward_user_data_list[i].user_common_reward_id) if itemList: hero_exp = 2000 + int(synthesize_hero_log_data["log_exp"]) - self.game_data.item.remove_item(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id) + self.game_data.item.remove_item(req.user_id, req.material_common_reward_user_data_list[i].user_common_reward_id) if equipmentList: - equipment_data = self.game_data.item.get_user_equipment(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id) + equipment_data = self.game_data.item.get_user_equipment(req.user_id, req.material_common_reward_user_data_list[i].user_common_reward_id) hero_exp = int(equipment_data["enhancement_exp"]) + int(synthesize_hero_log_data["log_exp"]) - self.game_data.item.remove_equipment(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id) + self.game_data.item.remove_equipment(req.user_id, req.material_common_reward_user_data_list[i].user_common_reward_id) if heroList: - hero_data = self.game_data.item.get_hero_log(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id) + hero_data = self.game_data.item.get_hero_log(req.user_id, req.material_common_reward_user_data_list[i].user_common_reward_id) hero_exp = int(hero_data["log_exp"]) + int(synthesize_hero_log_data["log_exp"]) - self.game_data.item.remove_hero_log(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id) + self.game_data.item.remove_hero_log(req.user_id, req.material_common_reward_user_data_list[i].user_common_reward_id) self.game_data.item.put_hero_log( - user_id, - int(req_data.origin_user_hero_log_id), + req.user_id, + int(req.origin_user_hero_log_id), synthesize_hero_log_data["log_level"], hero_exp, synthesize_hero_log_data["main_weapon"], @@ -402,13 +280,13 @@ class SaoBase: synthesize_hero_log_data["skill_slot5_skill_id"] ) - profile = self.game_data.profile.get_profile(req_data.user_id) + profile = self.game_data.profile.get_profile(req.user_id) new_col = int(profile["own_col"]) - 100 # Update profile self.game_data.profile.put_profile( - req_data.user_id, + req.user_id, profile["user_type"], profile["nick_name"], profile["rank_num"], @@ -420,12 +298,12 @@ class SaoBase: ) # Load the item again to push to the response handler - synthesize_hero_log_data = self.game_data.item.get_hero_log(req_data.user_id, req_data.origin_user_hero_log_id) + synthesize_hero_log_data = self.game_data.item.get_hero_log(req.user_id, req.origin_user_hero_log_id) - resp = SaoSynthesizeEnhancementHeroLogResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, synthesize_hero_log_data) + resp = SaoSynthesizeEnhancementHeroLogResponse(header.cmd +1, synthesize_hero_log_data) return resp.make() - def handle_c816(self, request: Any) -> bytes: + def handle_c816(self, header: SaoRequestHeader, request: bytes) -> bytes: #custom/synthesize_enhancement_equipment req = bytes.fromhex(request)[24:] @@ -493,10 +371,10 @@ class SaoBase: # Load the item again to push to the response handler synthesize_equipment_data = self.game_data.item.get_user_equipment(req_data.user_id, req_data.origin_user_equipment_id) - resp = SaoSynthesizeEnhancementEquipmentResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, synthesize_equipment_data) + resp = SaoSynthesizeEnhancementEquipmentResponse(header.cmd +1, synthesize_equipment_data) return resp.make() - def handle_c806(self, request: Any) -> bytes: + def handle_c806(self, header: SaoRequestHeader, request: bytes) -> bytes: #custom/change_party req = bytes.fromhex(request)[24:] @@ -566,10 +444,10 @@ class SaoBase: self.game_data.item.put_hero_party(user_id, req_data.party_data_list[0].party_team_data_list[0].user_party_team_id, party_hero_list[0], party_hero_list[1], party_hero_list[2]) - resp = SaoNoopResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoNoopResponse(header.cmd +1) return resp.make() - def handle_c904(self, request: Any) -> bytes: + def handle_c904(self, header: SaoRequestHeader, request: bytes) -> bytes: #quest/episode_play_start req = bytes.fromhex(request)[24:] @@ -608,10 +486,10 @@ class SaoBase: req_data.play_start_request_data[0].quest_drop_boost_apply_flag ) - resp = SaoEpisodePlayStartResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data) + resp = SaoEpisodePlayStartResponse(header.cmd +1, profile_data) return resp.make() - def handle_c908(self, request: Any) -> bytes: # Level calculation missing for the profile and heroes + def handle_c908(self, header: SaoRequestHeader, request: bytes) -> bytes: # Level calculation missing for the profile and heroes #quest/episode_play_end req = bytes.fromhex(request)[24:] @@ -859,10 +737,10 @@ class SaoBase: self.game_data.item.create_end_session(user_id, episode_id, quest_clear_flag, json_data["data"]) - resp = SaoEpisodePlayEndResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoEpisodePlayEndResponse(header.cmd +1) return resp.make() - def handle_c914(self, request: Any) -> bytes: + def handle_c914(self, header: SaoRequestHeader, request: bytes) -> bytes: #quest/trial_tower_play_start req = bytes.fromhex(request)[24:] @@ -901,10 +779,10 @@ class SaoBase: req_data.play_start_request_data[0].quest_drop_boost_apply_flag ) - resp = SaoEpisodePlayStartResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data) + resp = SaoEpisodePlayStartResponse(header.cmd +1, profile_data) return resp.make() - def handle_c918(self, request: Any) -> bytes: + def handle_c918(self, header: SaoRequestHeader, request: bytes) -> bytes: #quest/trial_tower_play_end req = bytes.fromhex(request)[24:] @@ -1169,10 +1047,10 @@ class SaoBase: self.game_data.item.create_end_session(user_id, trial_tower_id, quest_clear_flag, json_data["data"]) - resp = SaoTrialTowerPlayEndResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoTrialTowerPlayEndResponse(header.cmd +1) return resp.make() - def handle_c90a(self, request: Any) -> bytes: + def handle_c90a(self, header: SaoRequestHeader, request: bytes) -> bytes: #quest/episode_play_end_unanalyzed_log_fixed req = bytes.fromhex(request)[24:] @@ -1190,10 +1068,10 @@ class SaoBase: end_session_data = self.game_data.item.get_end_session(user_id) - resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, end_session_data[4]) + resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(header.cmd +1, end_session_data[4]) return resp.make() - def handle_c91a(self, request: Any) -> bytes: # handler is identical to the episode + def handle_c91a(self, header: SaoRequestHeader, request: bytes) -> bytes: # handler is identical to the episode #quest/trial_tower_play_end_unanalyzed_log_fixed req = bytes.fromhex(request)[24:] @@ -1210,35 +1088,35 @@ class SaoBase: end_session_data = self.game_data.item.get_end_session(user_id) - resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, end_session_data[4]) + resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(header.cmd +1, end_session_data[4]) return resp.make() - def handle_cd00(self, request: Any) -> bytes: + def handle_cd00(self, header: SaoRequestHeader, request: bytes) -> bytes: #defrag_match/get_defrag_match_basic_data - resp = SaoGetDefragMatchBasicDataResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoGetDefragMatchBasicDataResponse(header.cmd +1) return resp.make() - def handle_cd02(self, request: Any) -> bytes: + def handle_cd02(self, header: SaoRequestHeader, request: bytes) -> bytes: #defrag_match/get_defrag_match_ranking_user_data - resp = SaoGetDefragMatchRankingUserDataResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoGetDefragMatchRankingUserDataResponse(header.cmd +1) return resp.make() - def handle_cd04(self, request: Any) -> bytes: + def handle_cd04(self, header: SaoRequestHeader, request: bytes) -> bytes: #defrag_match/get_defrag_match_league_point_ranking_list - resp = SaoGetDefragMatchLeaguePointRankingListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoGetDefragMatchLeaguePointRankingListResponse(header.cmd +1) return resp.make() - def handle_cd06(self, request: Any) -> bytes: + def handle_cd06(self, header: SaoRequestHeader, request: bytes) -> bytes: #defrag_match/get_defrag_match_league_score_ranking_list - resp = SaoGetDefragMatchLeagueScoreRankingListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoGetDefragMatchLeagueScoreRankingListResponse(header.cmd +1) return resp.make() - def handle_d404(self, request: Any) -> bytes: + def handle_d404(self, header: SaoRequestHeader, request: bytes) -> bytes: #other/bnid_serial_code_check - resp = SaoBnidSerialCodeCheckResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoBnidSerialCodeCheckResponse(header.cmd +1) return resp.make() - def handle_c306(self, request: Any) -> bytes: + def handle_c306(self, header: SaoRequestHeader, request: bytes) -> bytes: #card/scan_qr_quest_profile_card - resp = SaoScanQrQuestProfileCardResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoScanQrQuestProfileCardResponse(header.cmd +1) return resp.make() \ No newline at end of file diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py index 3579a9e..99ab53d 100644 --- a/titles/sao/handlers/base.py +++ b/titles/sao/handlers/base.py @@ -1,5 +1,6 @@ import struct from datetime import datetime +from typing import List from construct import * from .helpers import * import csv @@ -202,6 +203,28 @@ class SaoCommonPayingPlayStartRequest(SaoBaseResponse): class SaoGetAuthCardDataRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) + off = 0 + self.cabinet_type = decode_byte(data, off) + off += BYTE_OFF + + self.auth_type = decode_byte(data, off) + off += BYTE_OFF + + store_id = decode_str(data, off) + self.store_id = store_id[0] + off += store_id[1] + + serial_no = decode_str(data, off) + self.serial_no = serial_no[0] + off += serial_no[1] + + access_code = decode_str(data, off) + self.access_code = access_code[0] + off += access_code[1] + + chip_id = decode_str(data, off) + self.chip_id = chip_id[0] + off += chip_id[1] class SaoGetAuthCardDataResponse(SaoBaseResponse): #GssSite.dll / GssSiteSystem / GameConnectProt / public class get_auth_card_data_R : GameConnect.GssProtocolBase def __init__(self, cmd, profile_data) -> None: @@ -339,6 +362,35 @@ class SaoTicketResponse(SaoBaseResponse): class SaoCommonLoginRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) + off = 0 + self.cabinet_type = decode_byte(data, off) + off += BYTE_OFF + + self.auth_type = decode_byte(data, off) + off += BYTE_OFF + + store_id = decode_str(data, off) + self.store_id = store_id[0] + off += store_id[1] + + store_name = decode_str(data, off) + self.store_name = store_name[0] + off += store_name[1] + + serial_no = decode_str(data, off) + self.serial_no = serial_no[0] + off += serial_no[1] + + access_code = decode_str(data, off) + self.access_code = access_code[0] + off += access_code[1] + + chip_id = decode_str(data, off) + self.chip_id = chip_id[0] + off += chip_id[1] + + self.free_ticket_distribution_target_flag = decode_byte(data, off) + off += BYTE_OFF class SaoCommonLoginResponse(SaoBaseResponse): def __init__(self, cmd, profile_data) -> None: @@ -413,6 +465,7 @@ class SaoCheckComebackEventRequest(SaoBaseResponse): class SaoGetUserBasicDataRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) + self.user_id = decode_str(data, 0)[0] class SaoGetUserBasicDataResponse(SaoBaseResponse): def __init__(self, cmd, profile_data) -> None: @@ -497,6 +550,7 @@ class SaoGetUserBasicDataResponse(SaoBaseResponse): class SaoGetHeroLogUserDataListRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) + self.user_id = decode_str(data, 0)[0] class SaoGetHeroLogUserDataListResponse(SaoBaseResponse): def __init__(self, cmd, hero_data) -> None: @@ -675,6 +729,7 @@ class SaoGetHeroLogUserDataListResponse(SaoBaseResponse): class SaoGetEquipmentUserDataListRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) + self.user_id = decode_str(data, 0)[0] class SaoGetEquipmentUserDataListResponse(SaoBaseResponse): def __init__(self, cmd, equipment_data) -> None: @@ -831,6 +886,7 @@ class SaoGetEquipmentUserDataListResponse(SaoBaseResponse): class SaoGetItemUserDataListRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) + self.user_id = decode_str(data, 0)[0] class SaoGetItemUserDataListResponse(SaoBaseResponse): def __init__(self, cmd, item_data) -> None: @@ -1014,6 +1070,7 @@ class SaoGetTitleUserDataListResponse(SaoBaseResponse): class SaoGetEpisodeAppendDataListRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) + self.user_id = decode_str(data, 0)[0] class SaoGetEpisodeAppendDataListResponse(SaoBaseResponse): def __init__(self, cmd, profile_data) -> None: @@ -1072,6 +1129,7 @@ class SaoGetEpisodeAppendDataListResponse(SaoBaseResponse): class SaoGetPartyDataListRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) + self.user_id = decode_str(data, 0)[0] class SaoGetPartyDataListResponse(SaoBaseResponse): # Default party def __init__(self, cmd, hero1_data, hero2_data, hero3_data) -> None: @@ -1756,6 +1814,7 @@ class SaoEpisodePlayEndUnanalyzedLogFixedResponse(SaoBaseResponse): class SaoGetQuestSceneUserDataListRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) + self.user_id = decode_str(data, 0)[0] class SaoGetQuestSceneUserDataListResponse(SaoBaseResponse): def __init__(self, cmd, quest_data) -> None: @@ -1973,6 +2032,34 @@ class SaoCheckProfileCardUsedRewardResponse(SaoBaseResponse): class SaoSynthesizeEnhancementHeroLogRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) + off = 0 + ticket_id = decode_str(data, 0) + self.ticket_id = ticket_id[0] + off += ticket_id[1] + + user_id = decode_str(data, 0) + self.user_id = user_id[0] + off += user_id[1] + + origin_user_hero_log_id = decode_str(data, 0) + self.origin_user_hero_log_id = origin_user_hero_log_id[0] + off += origin_user_hero_log_id[1] + + self.material_common_reward_user_data_list: List[MaterialCommonRewardUserData] + + if len(data) <= off: + self.material_common_reward_user_data_count = 0 + return + + self.material_common_reward_user_data_count = decode_int(data, off) + off += INT_OFF + + + for _ in range(self.material_common_reward_user_data_count): + mat = MaterialCommonRewardUserData(data, off) + off += mat.get_size() + self.material_common_reward_user_data_list.append(mat) + class SaoSynthesizeEnhancementHeroLogResponse(SaoBaseResponse): def __init__(self, cmd, hero_data) -> None: diff --git a/titles/sao/handlers/helpers.py b/titles/sao/handlers/helpers.py index c4d4214..38823cc 100644 --- a/titles/sao/handlers/helpers.py +++ b/titles/sao/handlers/helpers.py @@ -47,4 +47,21 @@ def encode_str(s: str) -> bytes: return str_len_bytes + str_bytes except: logging.getLogger('sao').error(f"Failed to encode {s} as bytes!") - return b"" \ No newline at end of file + return b"" + +class BaseHelper: + def __init__(self, data: bytes, offset: int) -> None: + pass + + def get_size(self) -> int: + return 0 + +class MaterialCommonRewardUserData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + self.common_reward_type = decode_short(data, offset) + offset += SHORT_OFF + + self.user_common_reward_id = decode_short(data, offset) + + def get_size(self) -> int: + return SHORT_OFF + SHORT_OFF \ No newline at end of file diff --git a/titles/sao/index.py b/titles/sao/index.py index 24a7188..dfc97a4 100644 --- a/titles/sao/index.py +++ b/titles/sao/index.py @@ -113,17 +113,10 @@ class SaoServlet(BaseServlet): else: req_data = req_raw[40:] - handler = getattr(self.base, f"handle_{cmd_str}", None) - if handler is None: - self.logger.info(f"Generic Handler for {endpoint} - {cmd_str}") - self.logger.debug(f"Request: {req_raw.hex()}") - resp_thing = SaoNoopResponse(req_header.cmd + 1) - resp = resp_thing.make() - - else: - self.logger.info(f"Handler {endpoint} - {cmd_str} request") - self.logger.debug(f"Request: {req_raw.hex()}") - resp = handler(sao_request) + handler = getattr(self.base, f"handle_{cmd_str}", self.base.handle_noop) + self.logger.info(f"{endpoint} - {cmd_str} request") + self.logger.debug(f"Request: {req_raw.hex()}") + resp = handler(req_header, req_data) self.logger.debug(f"Response: {resp.hex()}") From a299ba98ac34db31e6d01fe7c1993b3b9ca60eff Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Thu, 9 Nov 2023 23:37:40 -0500 Subject: [PATCH 039/101] sao: fix erronius length errors --- titles/sao/handlers/base.py | 9 +-------- titles/sao/handlers/helpers.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py index 99ab53d..afaa2cb 100644 --- a/titles/sao/handlers/base.py +++ b/titles/sao/handlers/base.py @@ -19,9 +19,7 @@ class SaoRequestHeader: class SaoBaseRequest: def __init__(self, header: SaoRequestHeader, data: bytes) -> None: self.header = header - if self.header.data_len != len(data): - logging.getLogger('sao').error(f"Expected {self.header.data_len} data bytes byt got {len(data)}!") - # TODO: Raise an error here + # TODO: Length check class SaoResponseHeader: def __init__(self, cmd_id: int) -> None: @@ -2046,15 +2044,10 @@ class SaoSynthesizeEnhancementHeroLogRequest(SaoBaseRequest): off += origin_user_hero_log_id[1] self.material_common_reward_user_data_list: List[MaterialCommonRewardUserData] - - if len(data) <= off: - self.material_common_reward_user_data_count = 0 - return self.material_common_reward_user_data_count = decode_int(data, off) off += INT_OFF - for _ in range(self.material_common_reward_user_data_count): mat = MaterialCommonRewardUserData(data, off) off += mat.get_size() diff --git a/titles/sao/handlers/helpers.py b/titles/sao/handlers/helpers.py index 38823cc..ccedb33 100644 --- a/titles/sao/handlers/helpers.py +++ b/titles/sao/handlers/helpers.py @@ -40,6 +40,21 @@ def decode_str(data: bytes, offset: int) -> Tuple[str, int]: logging.getLogger('sao').error(f"Failed to parse {data[offset:]} as string!") return ("", 0) +def encode_byte(data: int) -> bytes: + return struct.pack("!B", data) + +def encode_short(data: int) -> bytes: + return struct.pack("!H", data) + +def encode_int(data: int) -> bytes: + return struct.pack("!I", data) + +def encode_long(data: int) -> bytes: + return struct.pack("!L", data) + +def encode_bigint(data: int) -> bytes: + return struct.pack("!Q", data) + def encode_str(s: str) -> bytes: try: str_bytes = s.encode("utf-16-le", errors="replace") From c3705427613adedc4372d74761a5d37578460eb8 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 10 Nov 2023 15:52:55 -0500 Subject: [PATCH 040/101] sao: fix req header --- titles/sao/handlers/base.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py index afaa2cb..16bc3a9 100644 --- a/titles/sao/handlers/base.py +++ b/titles/sao/handlers/base.py @@ -8,13 +8,15 @@ from csv import * class SaoRequestHeader: def __init__(self, data: bytes) -> None: - collection = struct.unpack_from("!H6xIII16sI", data) + collection = struct.unpack_from("!HHIIII16sI", data) self.cmd: int = collection[0] - self.vendor_id: int = collection[1] - self.game_id: int = collection[2] - self.version_id: int = collection[3] - self.hash: str = collection[4] - self.data_len: str = collection[5] + self.err_status = collection[1] + self.error_type = collection[2] + self.vendor_id: int = collection[3] + self.game_id: int = collection[4] + self.version_id: int = collection[5] + self.hash: str = collection[6] + self.data_len: str = collection[7] class SaoBaseRequest: def __init__(self, header: SaoRequestHeader, data: bytes) -> None: From 9fdd96b71749c83c7f96c2165d2fdfc52673e8ca Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 10 Nov 2023 18:01:16 -0500 Subject: [PATCH 041/101] sao: fix synthesize_enhancement_equipment --- titles/sao/base.py | 25 ++----------------------- titles/sao/handlers/base.py | 25 +++++++++++++++++++++++-- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/titles/sao/base.py b/titles/sao/base.py index 24dd22d..936f017 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -305,31 +305,10 @@ class SaoBase: def handle_c816(self, header: SaoRequestHeader, request: bytes) -> bytes: #custom/synthesize_enhancement_equipment - req = bytes.fromhex(request)[24:] - - req_struct = Struct( - Padding(20), - "ticket_id" / Bytes(1), # needs to be parsed as an int - Padding(1), - "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id - "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string - "origin_user_equipment_id_size" / Rebuild(Int32ub, len_(this.origin_user_equipment_id) * 2), # calculates the length of the origin_user_equipment_id - "origin_user_equipment_id" / PaddedString(this.origin_user_equipment_id_size, "utf_16_le"), # origin_user_equipment_id is a (zero) padded string - Padding(3), - "material_common_reward_user_data_list_length" / Rebuild(Int8ub, len_(this.material_common_reward_user_data_list)), # material_common_reward_user_data_list is a byte, - "material_common_reward_user_data_list" / Array(this.material_common_reward_user_data_list_length, Struct( - "common_reward_type" / Int16ub, # team_no is a byte - "user_common_reward_id_size" / Rebuild(Int32ub, len_(this.user_common_reward_id) * 2), # calculates the length of the user_common_reward_id - "user_common_reward_id" / PaddedString(this.user_common_reward_id_size, "utf_16_le"), # user_common_reward_id is a (zero) padded string - )), - ) - - req_data = req_struct.parse(req) - - user_id = req_data.user_id + req_data = SaoSynthesizeEnhancementEquipmentRequest(header, request) synthesize_equipment_data = self.game_data.item.get_user_equipment(req_data.user_id, req_data.origin_user_equipment_id) - for i in range(0,req_data.material_common_reward_user_data_list_length): + for i in range(0,req_data.material_common_reward_user_data_count): itemList = self.game_data.static.get_item_id(req_data.material_common_reward_user_data_list[i].user_common_reward_id) heroList = self.game_data.static.get_hero_id(req_data.material_common_reward_user_data_list[i].user_common_reward_id) diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py index 16bc3a9..d1c1efc 100644 --- a/titles/sao/handlers/base.py +++ b/titles/sao/handlers/base.py @@ -2055,7 +2055,6 @@ class SaoSynthesizeEnhancementHeroLogRequest(SaoBaseRequest): off += mat.get_size() self.material_common_reward_user_data_list.append(mat) - class SaoSynthesizeEnhancementHeroLogResponse(SaoBaseResponse): def __init__(self, cmd, hero_data) -> None: super().__init__(cmd) @@ -2207,9 +2206,31 @@ class SaoSynthesizeEnhancementHeroLogResponse(SaoBaseResponse): self.length = len(resp_data) return super().make() + resp_data -class SaoSynthesizeEnhancementEquipment(SaoBaseRequest): +class SaoSynthesizeEnhancementEquipmentRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) + off = 0 + ticket_id = decode_str(data, 0) + self.ticket_id = ticket_id[0] + off += ticket_id[1] + + user_id = decode_str(data, 0) + self.user_id = user_id[0] + off += user_id[1] + + origin_user_equipment_id = decode_str(data, 0) + self.origin_user_equipment_id = origin_user_equipment_id[0] + off += origin_user_equipment_id[1] + + self.material_common_reward_user_data_list: List[MaterialCommonRewardUserData] + + self.material_common_reward_user_data_count = decode_int(data, off) + off += INT_OFF + + for _ in range(self.material_common_reward_user_data_count): + mat = MaterialCommonRewardUserData(data, off) + off += mat.get_size() + self.material_common_reward_user_data_list.append(mat) class SaoSynthesizeEnhancementEquipmentResponse(SaoBaseResponse): def __init__(self, cmd, synthesize_equipment_data) -> None: From c4f2232457a3a39638edb019e616895aa86281d0 Mon Sep 17 00:00:00 2001 From: Midorica Date: Fri, 10 Nov 2023 20:32:24 -0500 Subject: [PATCH 042/101] fixing threading profile loading on CXB --- titles/cxb/base.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/titles/cxb/base.py b/titles/cxb/base.py index 172628d..fe583e6 100644 --- a/titles/cxb/base.py +++ b/titles/cxb/base.py @@ -198,14 +198,6 @@ class CxbBase: ).decode("utf-8") ) - def task_generateIndexData(self, versionindex: List[str], uid: int): - 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: range_start = data["loadrange"]["range"][0] range_end = data["loadrange"]["range"][1] @@ -279,9 +271,14 @@ class CxbBase: thread_ScoreData = Thread(target=CxbBase.task_generateScoreData(song, index, data1)) thread_ScoreData.start() - for v in index: - thread_IndexData = Thread(target=CxbBase.task_generateIndexData(self, versionindex, uid)) - thread_IndexData.start() + v_profile = self.data.profile.get_profile_index(0, uid, self.version) + v_profile_data = v_profile["data"] + + 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} From 0e739b2077358fa77ba7026abeccd33130eaa125 Mon Sep 17 00:00:00 2001 From: Midorica Date: Fri, 10 Nov 2023 23:20:30 -0500 Subject: [PATCH 043/101] adding SSL support for SAO --- titles/sao/index.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/titles/sao/index.py b/titles/sao/index.py index dfc97a4..63ad3f7 100644 --- a/titles/sao/index.py +++ b/titles/sao/index.py @@ -76,11 +76,13 @@ class SaoServlet(BaseServlet): return True def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]: - tport = Utils.get_title_port(self.core_cfg) - return ( - f"http://{self.core_cfg.title.hostname}:{tport}/", - f"{self.core_cfg.title.hostname}/", - ) + if not self.core_cfg.server.is_using_proxy and self.core_cfg.title.port_ssl: + return ( + f"https://{self.core_cfg.title.hostname}:{self.core_cfg.title.port_ssl}/", + f"{self.core_cfg.title.hostname}/", + ) + + return (f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/", "") def get_mucha_info(self, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str]: if not self.game_cfg.server.enable: From 773729dc0c03bd85f134a2097cdf2ee6748908fc Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 11 Nov 2023 01:11:41 -0500 Subject: [PATCH 044/101] sao: fix episode_play_end --- titles/sao/base.py | 183 ++----------------- titles/sao/handlers/base.py | 90 +++++++++- titles/sao/handlers/helpers.py | 311 ++++++++++++++++++++++++++++++++- 3 files changed, 405 insertions(+), 179 deletions(-) diff --git a/titles/sao/base.py b/titles/sao/base.py index 936f017..1379f03 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -355,49 +355,11 @@ class SaoBase: def handle_c806(self, header: SaoRequestHeader, request: bytes) -> bytes: #custom/change_party - req = bytes.fromhex(request)[24:] - - req_struct = Struct( - Padding(20), - "ticket_id" / Bytes(1), # needs to be parsed as an int - Padding(1), - "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id - "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string - "act_type" / Int8ub, # play_mode is a byte - Padding(3), - "party_data_list_length" / Rebuild(Int8ub, len_(this.party_data_list)), # party_data_list is a byte, - "party_data_list" / Array(this.party_data_list_length, Struct( - "user_party_id_size" / Rebuild(Int32ub, len_(this.user_party_id) * 2), # calculates the length of the user_party_id - "user_party_id" / PaddedString(this.user_party_id_size, "utf_16_le"), # user_party_id is a (zero) padded string - "team_no" / Int8ub, # team_no is a byte - Padding(3), - "party_team_data_list_length" / Rebuild(Int8ub, len_(this.party_team_data_list)), # party_team_data_list is a byte - "party_team_data_list" / Array(this.party_team_data_list_length, Struct( - "user_party_team_id_size" / Rebuild(Int32ub, len_(this.user_party_team_id) * 2), # calculates the length of the user_party_team_id - "user_party_team_id" / PaddedString(this.user_party_team_id_size, "utf_16_le"), # user_party_team_id is a (zero) padded string - "arrangement_num" / Int8ub, # arrangement_num is a byte - "user_hero_log_id_size" / Rebuild(Int32ub, len_(this.user_hero_log_id) * 2), # calculates the length of the user_hero_log_id - "user_hero_log_id" / PaddedString(this.user_hero_log_id_size, "utf_16_le"), # user_hero_log_id is a (zero) padded string - "main_weapon_user_equipment_id_size" / Rebuild(Int32ub, len_(this.main_weapon_user_equipment_id) * 2), # calculates the length of the main_weapon_user_equipment_id - "main_weapon_user_equipment_id" / PaddedString(this.main_weapon_user_equipment_id_size, "utf_16_le"), # main_weapon_user_equipment_id is a (zero) padded string - "sub_equipment_user_equipment_id_size" / Rebuild(Int32ub, len_(this.sub_equipment_user_equipment_id) * 2), # calculates the length of the sub_equipment_user_equipment_id - "sub_equipment_user_equipment_id" / PaddedString(this.sub_equipment_user_equipment_id_size, "utf_16_le"), # sub_equipment_user_equipment_id is a (zero) padded string - "skill_slot1_skill_id" / Int32ub, # skill_slot1_skill_id is a int, - "skill_slot2_skill_id" / Int32ub, # skill_slot1_skill_id is a int, - "skill_slot3_skill_id" / Int32ub, # skill_slot1_skill_id is a int, - "skill_slot4_skill_id" / Int32ub, # skill_slot1_skill_id is a int, - "skill_slot5_skill_id" / Int32ub, # skill_slot1_skill_id is a int, - )), - )), - - ) - - req_data = req_struct.parse(req) - user_id = req_data.user_id + req_data = SaoChangePartyRequest(header, request) party_hero_list = [] for party_team in req_data.party_data_list[0].party_team_data_list: - hero_data = self.game_data.item.get_hero_log(user_id, party_team["user_hero_log_id"]) + hero_data = self.game_data.item.get_hero_log(req_data.user_id, party_team["user_hero_log_id"]) hero_level = 1 hero_exp = 0 @@ -406,7 +368,7 @@ class SaoBase: hero_exp = hero_data["log_exp"] self.game_data.item.put_hero_log( - user_id, + req_data.user_id, party_team["user_hero_log_id"], hero_level, hero_exp, @@ -421,38 +383,14 @@ class SaoBase: party_hero_list.append(party_team["user_hero_log_id"]) - self.game_data.item.put_hero_party(user_id, req_data.party_data_list[0].party_team_data_list[0].user_party_team_id, party_hero_list[0], party_hero_list[1], party_hero_list[2]) + self.game_data.item.put_hero_party(req_data.user_id, req_data.party_data_list[0].party_team_data_list[0].user_party_team_id, party_hero_list[0], party_hero_list[1], party_hero_list[2]) resp = SaoNoopResponse(header.cmd +1) return resp.make() def handle_c904(self, header: SaoRequestHeader, request: bytes) -> bytes: #quest/episode_play_start - req = bytes.fromhex(request)[24:] - - req_struct = Struct( - Padding(20), - "ticket_id" / Bytes(1), # needs to be parsed as an int - Padding(1), - "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id - "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string - "episode_id" / Int32ub, # episode_id is a int, - "play_mode" / Int8ub, # play_mode is a byte - Padding(3), - "play_start_request_data_length" / Rebuild(Int8ub, len_(this.play_start_request_data)), # play_start_request_data_length is a byte, - "play_start_request_data" / Array(this.play_start_request_data_length, Struct( - "user_party_id_size" / Rebuild(Int32ub, len_(this.user_party_id) * 2), # calculates the length of the user_party_id - "user_party_id" / PaddedString(this.user_party_id_size, "utf_16_le"), # user_party_id is a (zero) padded string - "appoint_leader_resource_card_code_size" / Rebuild(Int32ub, len_(this.appoint_leader_resource_card_code) * 2), # calculates the length of the total_damage - "appoint_leader_resource_card_code" / PaddedString(this.appoint_leader_resource_card_code_size, "utf_16_le"), # total_damage is a (zero) padded string - "use_profile_card_code_size" / Rebuild(Int32ub, len_(this.use_profile_card_code) * 2), # calculates the length of the total_damage - "use_profile_card_code" / PaddedString(this.use_profile_card_code_size, "utf_16_le"), # use_profile_card_code is a (zero) padded string - "quest_drop_boost_apply_flag" / Int8ub, # quest_drop_boost_apply_flag is a byte - )), - - ) - - req_data = req_struct.parse(req) + req_data = SaoEpisodePlayStartRequest(header, request) user_id = req_data.user_id profile_data = self.game_data.profile.get_profile(user_id) @@ -471,108 +409,21 @@ class SaoBase: def handle_c908(self, header: SaoRequestHeader, request: bytes) -> bytes: # Level calculation missing for the profile and heroes #quest/episode_play_end - req = bytes.fromhex(request)[24:] - - req_struct = Struct( - Padding(20), - "ticket_id" / Bytes(1), # needs to be parsed as an int - Padding(1), - "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id - "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string - Padding(2), - "episode_id" / Int16ub, # episode_id is a short, - Padding(3), - "play_end_request_data" / Int8ub, # play_end_request_data is a byte - Padding(1), - "play_result_flag" / Int8ub, # play_result_flag is a byte - Padding(2), - "base_get_data_length" / Rebuild(Int8ub, len_(this.base_get_data)), # base_get_data_length is a byte, - "base_get_data" / Array(this.base_get_data_length, Struct( - "get_hero_log_exp" / Int32ub, # get_hero_log_exp is an int - "get_col" / Int32ub, # get_num is a short - )), - Padding(3), - "get_player_trace_data_list_length" / Rebuild(Int8ub, len_(this.get_player_trace_data_list)), # get_player_trace_data_list_length is a byte - "get_player_trace_data_list" / Array(this.get_player_trace_data_list_length, Struct( - "user_quest_scene_player_trace_id" / Int32ub, # user_quest_scene_player_trace_id is an int - )), - Padding(3), - "get_rare_drop_data_list_length" / Rebuild(Int8ub, len_(this.get_rare_drop_data_list)), # get_rare_drop_data_list_length is a byte - "get_rare_drop_data_list" / Array(this.get_rare_drop_data_list_length, Struct( - "quest_rare_drop_id" / Int32ub, # quest_rare_drop_id is an int - )), - Padding(3), - "get_special_rare_drop_data_list_length" / Rebuild(Int8ub, len_(this.get_special_rare_drop_data_list)), # get_special_rare_drop_data_list_length is a byte - "get_special_rare_drop_data_list" / Array(this.get_special_rare_drop_data_list_length, Struct( - "quest_special_rare_drop_id" / Int32ub, # quest_special_rare_drop_id is an int - )), - Padding(3), - "get_unanalyzed_log_tmp_reward_data_list_length" / Rebuild(Int8ub, len_(this.get_unanalyzed_log_tmp_reward_data_list)), # get_unanalyzed_log_tmp_reward_data_list_length is a byte - "get_unanalyzed_log_tmp_reward_data_list" / Array(this.get_unanalyzed_log_tmp_reward_data_list_length, Struct( - "unanalyzed_log_grade_id" / Int32ub, # unanalyzed_log_grade_id is an int, - )), - Padding(3), - "get_event_item_data_list_length" / Rebuild(Int8ub, len_(this.get_event_item_data_list)), # get_event_item_data_list_length is a byte, - "get_event_item_data_list" / Array(this.get_event_item_data_list_length, Struct( - "event_item_id" / Int32ub, # event_item_id is an int - "get_num" / Int16ub, # get_num is a short - )), - Padding(3), - "discovery_enemy_data_list_length" / Rebuild(Int8ub, len_(this.discovery_enemy_data_list)), # discovery_enemy_data_list_length is a byte - "discovery_enemy_data_list" / Array(this.discovery_enemy_data_list_length, Struct( - "enemy_kind_id" / Int32ub, # enemy_kind_id is an int - "destroy_num" / Int16ub, # destroy_num is a short - )), - Padding(3), - "destroy_boss_data_list_length" / Rebuild(Int8ub, len_(this.destroy_boss_data_list)), # destroy_boss_data_list_length is a byte - "destroy_boss_data_list" / Array(this.destroy_boss_data_list_length, Struct( - "boss_type" / Int8ub, # boss_type is a byte - "enemy_kind_id" / Int32ub, # enemy_kind_id is an int - "destroy_num" / Int16ub, # destroy_num is a short - )), - Padding(3), - "mission_data_list_length" / Rebuild(Int8ub, len_(this.mission_data_list)), # mission_data_list_length is a byte - "mission_data_list" / Array(this.mission_data_list_length, Struct( - "mission_id" / Int32ub, # enemy_kind_id is an int - "clear_flag" / Int8ub, # boss_type is a byte - "mission_difficulty_id" / Int16ub, # destroy_num is a short - )), - Padding(3), - "score_data_length" / Rebuild(Int8ub, len_(this.score_data)), # score_data_length is a byte - "score_data" / Array(this.score_data_length, Struct( - "clear_time" / Int32ub, # clear_time is an int - "combo_num" / Int32ub, # boss_type is a int - "total_damage_size" / Rebuild(Int32ub, len_(this.total_damage) * 2), # calculates the length of the total_damage - "total_damage" / PaddedString(this.total_damage_size, "utf_16_le"), # total_damage is a (zero) padded string - "concurrent_destroying_num" / Int16ub, # concurrent_destroying_num is a short - "reaching_skill_level" / Int16ub, # reaching_skill_level is a short - "ko_chara_num" / Int8ub, # ko_chara_num is a byte - "acceleration_invocation_num" / Int16ub, # acceleration_invocation_num is a short - "boss_destroying_num" / Int16ub, # boss_destroying_num is a short - "synchro_skill_used_flag" / Int8ub, # synchro_skill_used_flag is a byte - "used_friend_skill_id" / Int32ub, # used_friend_skill_id is an int - "friend_skill_used_flag" / Int8ub, # friend_skill_used_flag is a byte - "continue_cnt" / Int16ub, # continue_cnt is a short - "total_loss_num" / Int16ub, # total_loss_num is a short - )), - - ) - - req_data = req_struct.parse(req) + req_data = SaoEpisodePlayEndRequest(header, request) # Add stage progression to database user_id = req_data.user_id episode_id = req_data.episode_id - quest_clear_flag = bool(req_data.score_data[0].boss_destroying_num) - clear_time = req_data.score_data[0].clear_time - combo_num = req_data.score_data[0].combo_num - total_damage = req_data.score_data[0].total_damage - concurrent_destroying_num = req_data.score_data[0].concurrent_destroying_num + quest_clear_flag = bool(req_data.play_end_request_data_list[0].score_data_list[0].boss_destroying_num) + clear_time = req_data.play_end_request_data_list[0].score_data_list[0].clear_time + combo_num = req_data.play_end_request_data_list[0].score_data_list[0].combo_num + total_damage = req_data.play_end_request_data_list[0].score_data_list[0].total_damage + concurrent_destroying_num = req_data.play_end_request_data_list[0].score_data_list[0].concurrent_destroying_num profile = self.game_data.profile.get_profile(user_id) vp = int(profile["own_vp"]) exp = int(profile["rank_exp"]) + 100 #always 100 extra exp for some reason - col = int(profile["own_col"]) + int(req_data.base_get_data[0].get_col) + col = int(profile["own_col"]) + int(req_data.play_end_request_data_list[0].base_get_data_list[0].get_col) if quest_clear_flag is True: # Save stage progression - to be revised to avoid saving worse score @@ -638,7 +489,7 @@ class SaoBase: for i in range(0,len(hero_list)): hero_data = self.game_data.item.get_hero_log(user_id, hero_list[i]) - log_exp = int(hero_data["log_exp"]) + int(req_data.base_get_data[0].get_hero_log_exp) + log_exp = int(hero_data["log_exp"]) + int(req_data.play_end_request_data_list[0].base_get_data_list[0].get_hero_log_exp) # Calculate hero level based off experience and the CSV list with open(r'titles/sao/data/HeroLogLevel.csv') as csv_file: @@ -674,8 +525,8 @@ class SaoBase: # Grab the rare loot from the table, match it with the right item and then push to the player profile json_data = {"data": []} - for r in range(0,req_data.get_rare_drop_data_list_length): - rewardList = self.game_data.static.get_rare_drop_id(int(req_data.get_rare_drop_data_list[r].quest_rare_drop_id)) + for r in range(0,req_data.play_end_request_data_list[0].get_rare_drop_data_list): + rewardList = self.game_data.static.get_rare_drop_id(int(req_data.play_end_request_data_list[0].get_rare_drop_data_list[r].quest_rare_drop_id)) commonRewardId = rewardList["commonRewardId"] heroList = self.game_data.static.get_hero_id(commonRewardId) @@ -690,14 +541,14 @@ class SaoBase: self.game_data.item.put_item(user_id, commonRewardId) # Generate random hero(es) based off the response - for a in range(0,req_data.get_unanalyzed_log_tmp_reward_data_list_length): + for a in range(0,req_data.play_end_request_data_list[0].get_unanalyzed_log_tmp_reward_data_count): with open('titles/sao/data/RewardTable.csv', 'r') as f: keys_unanalyzed = next(f).strip().split(',') data_unanalyzed = list(DictReader(f, fieldnames=keys_unanalyzed)) randomized_unanalyzed_id = choice(data_unanalyzed) - while int(randomized_unanalyzed_id['UnanalyzedLogGradeId']) != req_data.get_unanalyzed_log_tmp_reward_data_list[a].unanalyzed_log_grade_id: + while int(randomized_unanalyzed_id['UnanalyzedLogGradeId']) != req_data.play_end_request_data_list[0].get_unanalyzed_log_tmp_reward_data_list[a].unanalyzed_log_grade_id: randomized_unanalyzed_id = choice(data_unanalyzed) heroList = self.game_data.static.get_hero_id(randomized_unanalyzed_id['CommonRewardId']) diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py index d1c1efc..0b5c522 100644 --- a/titles/sao/handlers/base.py +++ b/titles/sao/handlers/base.py @@ -1473,6 +1473,29 @@ class SaoGetResourcePathInfoResponse(SaoBaseResponse): class SaoEpisodePlayStartRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) + off = 0 + ticket_id = decode_str(data, off) + self.ticket_id = ticket_id[0] + off += ticket_id[1] + + user_id = decode_str(data, off) + self.user_id = user_id[0] + off += user_id[1] + + self.episode_id = decode_int(data, off) + off += INT_OFF + + self.play_mode = decode_byte(data, off) + off += BYTE_OFF + + self.play_start_request_data_count = decode_int(data, off) + off += INT_OFF + + self.play_start_request_data: List[PlayStartRequestData] = [] + for _ in range(self.play_start_request_data_count): + tmp = PlayStartRequestData(data, off) + self.play_start_request_data.append(tmp) + off += tmp.get_size() class SaoEpisodePlayStartResponse(SaoBaseResponse): def __init__(self, cmd, profile_data) -> None: @@ -1520,6 +1543,35 @@ class SaoEpisodePlayStartResponse(SaoBaseResponse): class SaoEpisodePlayEndRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) + off = 0 + ticket_id = decode_str(data, off) + self.ticket_id = ticket_id[0] + off += ticket_id[1] + + user_id = decode_str(data, off) + self.user_id = user_id[0] + off += user_id[1] + + self.episode_id = decode_int(data, off) + off += INT_OFF + + self.play_end_request_data_count = decode_int(data, off) + off += INT_OFF + + self.play_end_request_data_list: List[PlayEndRequestData] = [] + for _ in range(self.play_end_request_data_count): + tmp = PlayEndRequestData(data, off) + off += tmp.get_size() + self.play_end_request_data_list.append(tmp) + + self.multi_play_end_request_data_count = decode_int(data, off) + off += INT_OFF + + self.multi_play_end_request_data_list: List[MultiPlayEndRequestData] = [] + for _ in range(self.multi_play_end_request_data_count): + tmp = MultiPlayEndRequestData(data, off) + off += tmp.get_size() + self.multi_play_end_request_data_list.append(tmp) class SaoEpisodePlayEndResponse(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -2033,15 +2085,15 @@ class SaoSynthesizeEnhancementHeroLogRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) off = 0 - ticket_id = decode_str(data, 0) + ticket_id = decode_str(data, off) self.ticket_id = ticket_id[0] off += ticket_id[1] - user_id = decode_str(data, 0) + user_id = decode_str(data, off) self.user_id = user_id[0] off += user_id[1] - origin_user_hero_log_id = decode_str(data, 0) + origin_user_hero_log_id = decode_str(data, off) self.origin_user_hero_log_id = origin_user_hero_log_id[0] off += origin_user_hero_log_id[1] @@ -2210,15 +2262,15 @@ class SaoSynthesizeEnhancementEquipmentRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) off = 0 - ticket_id = decode_str(data, 0) + ticket_id = decode_str(data, off) self.ticket_id = ticket_id[0] off += ticket_id[1] - user_id = decode_str(data, 0) + user_id = decode_str(data, off) self.user_id = user_id[0] off += user_id[1] - origin_user_equipment_id = decode_str(data, 0) + origin_user_equipment_id = decode_str(data, off) self.origin_user_equipment_id = origin_user_equipment_id[0] off += origin_user_equipment_id[1] @@ -2885,4 +2937,28 @@ class SaoConsumeCreditGuestRequest(SaoBaseRequest): self.consume_num = decode_byte(data, off) off += BYTE_OFF - \ No newline at end of file + +class SaoChangePartyRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + ticket_id = decode_str(data, off) + self.ticket_id = ticket_id[0] + off += ticket_id[1] + + user_id = decode_str(data, off) + self.user_id = user_id[0] + off += user_id[1] + + self.act_type = decode_byte(data, off) + off += BYTE_OFF + + self.party_data_count = decode_int(data, off) + off += INT_OFF + + self.party_data_list: List[PartyData] = [] + + for _ in range(self.party_data_count): + tmp = PartyData(data, off) + self.party_data_list.append(tmp) + off += tmp.get_size() diff --git a/titles/sao/handlers/helpers.py b/titles/sao/handlers/helpers.py index ccedb33..9c5f25a 100644 --- a/titles/sao/handlers/helpers.py +++ b/titles/sao/handlers/helpers.py @@ -1,4 +1,4 @@ -from typing import Tuple +from typing import Tuple, List import struct import logging @@ -66,10 +66,10 @@ def encode_str(s: str) -> bytes: class BaseHelper: def __init__(self, data: bytes, offset: int) -> None: - pass + self._sz = 0 def get_size(self) -> int: - return 0 + return self._sz class MaterialCommonRewardUserData(BaseHelper): def __init__(self, data: bytes, offset: int) -> None: @@ -77,6 +77,305 @@ class MaterialCommonRewardUserData(BaseHelper): offset += SHORT_OFF self.user_common_reward_id = decode_short(data, offset) - - def get_size(self) -> int: - return SHORT_OFF + SHORT_OFF \ No newline at end of file + + self._sz = SHORT_OFF + SHORT_OFF + +class PartyTeamData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + sz = 0 + user_party_team_id = decode_str(data, offset + sz) + self.user_party_team_id = user_party_team_id[0] + sz += user_party_team_id[1] + + self.arrangement_num = decode_byte(data, offset + sz) + sz += BYTE_OFF + + user_hero_log_id = decode_str(data, offset + sz) + self.user_hero_log_id = user_hero_log_id[0] + sz += user_hero_log_id[1] + + main_weapon_user_equipment_id = decode_str(data, offset + sz) + self.main_weapon_user_equipment_id = main_weapon_user_equipment_id[0] + sz += main_weapon_user_equipment_id[1] + + sub_equipment_user_equipment_id = decode_str(data, offset + sz) + self.sub_equipment_user_equipment_id = sub_equipment_user_equipment_id[0] + sz += sub_equipment_user_equipment_id[1] + + self.skill_slot1_skill_id = decode_int(data, offset + sz) + sz += INT_OFF + + self.skill_slot2_skill_id = decode_int(data, offset + sz) + sz += INT_OFF + + self.skill_slot3_skill_id = decode_int(data, offset + sz) + sz += INT_OFF + + self.skill_slot4_skill_id = decode_int(data, offset + sz) + sz += INT_OFF + + self.skill_slot5_skill_id = decode_int(data, offset + sz) + sz += INT_OFF + + self._sz = sz + +class PartyData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + sz = 0 + user_party_id = decode_str(data, offset + sz) + self.user_party_id = user_party_id[0] + sz += user_party_id[1] + + self.team_no = decode_byte(data, offset + sz) + sz += BYTE_OFF + + self.party_team_data_count = decode_int(data, offset + sz) + sz += INT_OFF + + self.party_team_data_list: List[PartyTeamData] = [] + for _ in range(self.party_team_data_count): + tmp = PartyTeamData(data, offset + sz) + self.party_team_data_list.append(tmp) + sz += tmp.get_size() + + self._sz = sz + +class PlayStartRequestData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + sz = 0 + user_party_id = decode_str(data, offset + sz) + self.user_party_id = user_party_id[0] + sz += user_party_id[1] + + appoint_leader_resource_card_code = decode_str(data, offset + sz) + self.appoint_leader_resource_card_code = appoint_leader_resource_card_code[0] + sz += appoint_leader_resource_card_code[1] + + use_profile_card_code = decode_str(data, offset + sz) + self.use_profile_card_code = use_profile_card_code[0] + sz += use_profile_card_code[1] + + self.quest_drop_boost_apply_flag = decode_byte(data, offset + sz) + sz += BYTE_OFF + + self._sz = sz + +class GetPlayerTraceData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + user_quest_scene_player_trace_id = decode_str(data, offset) + self.user_quest_scene_player_trace_id = user_quest_scene_player_trace_id[0] + self._sz = user_quest_scene_player_trace_id[1] + +class BaseGetData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + self.get_hero_log_exp = decode_int(data, offset) + offset += INT_OFF + + self.get_col = decode_int(data, offset) + + self._sz = INT_OFF + INT_OFF + +class RareDropData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + self.quest_rare_drop_id = decode_int(data, offset) + + self._sz = INT_OFF + +class UnanalyzedLogTmpRewardData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + self.unanalyzed_log_grade_id = decode_int(data, offset) + + self._sz = INT_OFF + +class SpecialRareDropData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + self.quest_special_rare_drop_id = decode_int(data, offset) + + self._sz = INT_OFF + +class EventItemData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + self.event_item_id = decode_int(data, offset) + offset += INT_OFF + self.get_num = decode_short(data, offset) + + self._sz = INT_OFF + SHORT_OFF + +class DiscoveryEnemyData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + self.enemy_kind_id = decode_int(data, offset) + offset += INT_OFF + self.destroy_num = decode_short(data, offset) + + self._sz = INT_OFF + SHORT_OFF + +class DestroyBossData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + self.boss_type = decode_byte(data, offset) + offset += BYTE_OFF + self.enemy_kind_id = decode_int(data, offset) + offset += INT_OFF + self.mission_difficulty_id = decode_short(data, offset) + + self._sz = INT_OFF + SHORT_OFF + BYTE_OFF + +class MissionData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + self.mission_id = decode_int(data, offset) + offset += INT_OFF + self.clear_flag = decode_byte(data, offset) + offset += BYTE_OFF + self.destroy_num = decode_short(data, offset) + + self._sz = INT_OFF + SHORT_OFF + BYTE_OFF + +class ScoreData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + self.clear_time = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + self.combo_num = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + total_damage = decode_str(data, offset + self._sz) + self.total_damage = total_damage[0] + self._sz += total_damage[1] + + self.concurrent_destroying_num = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + + self.reaching_skill_level = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + + self.ko_chara_num = decode_byte(data, offset + self._sz) + self._sz += BYTE_OFF + + self.acceleration_invocation_num = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + + self.boss_destroying_num = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + + self.synchro_skill_used_flag = decode_byte(data, offset + self._sz) + self._sz += BYTE_OFF + + self.used_friend_skill_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + self.friend_skill_used_flag = decode_byte(data, offset + self._sz) + self._sz += BYTE_OFF + + self.continue_cnt = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + + self.total_loss_num = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + +class PlayEndRequestData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + sz = 0 + self.play_result_flag = decode_byte(data, offset + sz) + sz += BYTE_OFF + + self.base_get_data_count = decode_int(data, offset + sz) + sz += INT_OFF + + self.base_get_data_list: List[BaseGetData] = [] + for _ in range(self.base_get_data_count): + tmp = BaseGetData(data, sz) + sz += tmp.get_size() + self.base_get_data_list.append(tmp) + + self.get_player_trace_data_count = decode_int(data, offset + sz) + sz += INT_OFF + + self.get_player_trace_data: List[GetPlayerTraceData] = [] + for _ in range(self.get_player_trace_data_count): + tmp = GetPlayerTraceData(data, sz) + sz += tmp.get_size() + self.get_player_trace_data.append(tmp) + + self.get_rare_drop_data_count = decode_int(data, offset + sz) + sz += INT_OFF + + self.get_rare_drop_data_list: List[RareDropData] = [] + for _ in range(self.get_rare_drop_data_count): + tmp = RareDropData(data, sz) + sz += tmp.get_size() + self.get_rare_drop_data_list.append(tmp) + + self.get_special_rare_drop_data_count = decode_int(data, offset + sz) + sz += INT_OFF + + self.get_special_rare_drop_data_list: List[SpecialRareDropData] = [] + for _ in range(self.get_special_rare_drop_data_count): + tmp = SpecialRareDropData(data, sz) + sz += tmp.get_size() + self.get_special_rare_drop_data_list.append(tmp) + + self.get_unanalyzed_log_tmp_reward_data_count = decode_int(data, offset + sz) + sz += INT_OFF + + self.get_unanalyzed_log_tmp_reward_data_list: List[UnanalyzedLogTmpRewardData] = [] + for _ in range(self.get_unanalyzed_log_tmp_reward_data_count): + tmp = UnanalyzedLogTmpRewardData(data, sz) + sz += tmp.get_size() + self.get_unanalyzed_log_tmp_reward_data_list.append(tmp) + + self.get_event_item_data_count = decode_int(data, offset + sz) + sz += INT_OFF + + self.get_event_item_data_list: List[EventItemData] = [] + for _ in range(self.get_event_item_data_count): + tmp = EventItemData(data, sz) + sz += tmp.get_size() + self.get_event_item_data_list.append(tmp) + + self.discovery_enemy_data_count = decode_int(data, offset + sz) + sz += INT_OFF + + self.discovery_enemy_data_list: List[DiscoveryEnemyData] = [] + for _ in range(self.discovery_enemy_data_count): + tmp = DiscoveryEnemyData(data, sz) + sz += tmp.get_size() + self.discovery_enemy_data_list.append(tmp) + + self.destroy_boss_data_count = decode_int(data, offset + sz) + sz += INT_OFF + + self.destroy_boss_data_list: List[DestroyBossData] = [] + for _ in range(self.destroy_boss_data_count): + tmp = DestroyBossData(data, sz) + sz += tmp.get_size() + self.destroy_boss_data_list.append(tmp) + + self.mission_data_count = decode_int(data, offset + sz) + sz += INT_OFF + + self.mission_data_list: List[MissionData] = [] + for _ in range(self.mission_data_count): + tmp = MissionData(data, sz) + sz += tmp.get_size() + self.mission_data_list.append(tmp) + + self.score_data_count = decode_int(data, offset + sz) + sz += INT_OFF + + self.score_data_list: List[ScoreData] = [] + for _ in range(self.score_data_count): + tmp = ScoreData(data, sz) + sz += tmp.get_size() + self.score_data_list.append(tmp) + + self._sz = sz + +class MultiPlayEndRequestData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + self.dummy_1 = decode_byte(data, offset + self._sz) + self._sz += BYTE_OFF + self.dummy_2 = decode_byte(data, offset + self._sz) + self._sz += BYTE_OFF + self.dummy_3 = decode_byte(data, offset + self._sz) + self._sz += BYTE_OFF From c23ab1438e3614cdfad877e4c32ebf0dc22b1254 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 11 Nov 2023 01:13:09 -0500 Subject: [PATCH 045/101] sao: remove unused imports --- titles/sao/base.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/titles/sao/base.py b/titles/sao/base.py index 1379f03..777ff3c 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -1,11 +1,6 @@ -from datetime import datetime, timedelta -import json, logging -from typing import Any, Dict -import random -import struct +import logging from csv import * from random import choice -import random as rand from core.data import Data from core import CoreConfig From 1ff0e4e2c69fbd23bebdd6ad2278f865f458ba39 Mon Sep 17 00:00:00 2001 From: Midorica Date: Sat, 11 Nov 2023 11:10:57 -0500 Subject: [PATCH 046/101] sao: fix episode_play_end again --- titles/sao/base.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/titles/sao/base.py b/titles/sao/base.py index 777ff3c..e1c73ef 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -520,7 +520,7 @@ class SaoBase: # Grab the rare loot from the table, match it with the right item and then push to the player profile json_data = {"data": []} - for r in range(0,req_data.play_end_request_data_list[0].get_rare_drop_data_list): + for r in range(0,req_data.play_end_request_data_list[0].get_rare_drop_data_count): rewardList = self.game_data.static.get_rare_drop_id(int(req_data.play_end_request_data_list[0].get_rare_drop_data_list[r].quest_rare_drop_id)) commonRewardId = rewardList["commonRewardId"] @@ -537,15 +537,11 @@ class SaoBase: # Generate random hero(es) based off the response for a in range(0,req_data.play_end_request_data_list[0].get_unanalyzed_log_tmp_reward_data_count): - with open('titles/sao/data/RewardTable.csv', 'r') as f: keys_unanalyzed = next(f).strip().split(',') data_unanalyzed = list(DictReader(f, fieldnames=keys_unanalyzed)) randomized_unanalyzed_id = choice(data_unanalyzed) - while int(randomized_unanalyzed_id['UnanalyzedLogGradeId']) != req_data.play_end_request_data_list[0].get_unanalyzed_log_tmp_reward_data_list[a].unanalyzed_log_grade_id: - randomized_unanalyzed_id = choice(data_unanalyzed) - heroList = self.game_data.static.get_hero_id(randomized_unanalyzed_id['CommonRewardId']) equipmentList = self.game_data.static.get_equipment_id(randomized_unanalyzed_id['CommonRewardId']) itemList = self.game_data.static.get_item_id(randomized_unanalyzed_id['CommonRewardId']) From e1a70d1a06ab374f312533db3df18af9e1ea7a3e Mon Sep 17 00:00:00 2001 From: Midorica Date: Sun, 12 Nov 2023 16:13:04 -0500 Subject: [PATCH 047/101] sao: fix dict lengths of multiple response handlers --- titles/sao/handlers/base.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py index 0b5c522..db6e98f 100644 --- a/titles/sao/handlers/base.py +++ b/titles/sao/handlers/base.py @@ -720,6 +720,8 @@ class SaoGetHeroLogUserDataListResponse(SaoBaseResponse): resp_data.hero_log_user_data_list.append(hero_data) + resp_data["hero_log_user_data_list_size"] = len(resp_data.hero_log_user_data_list) + # finally, rebuild the resp_data resp_data = resp_struct.build(resp_data) @@ -877,6 +879,8 @@ class SaoGetEquipmentUserDataListResponse(SaoBaseResponse): resp_data.equipment_user_data_list.append(equipment_data) + resp_data["equipment_user_data_list_size"] = len(resp_data.equipment_user_data_list) + # finally, rebuild the resp_data resp_data = resp_struct.build(resp_data) @@ -942,6 +946,8 @@ class SaoGetItemUserDataListResponse(SaoBaseResponse): resp_data.item_user_data_list.append(item_data) + resp_data["item_user_data_list_size"] = len(resp_data.item_user_data_list) + # finally, rebuild the resp_data resp_data = resp_struct.build(resp_data) @@ -1014,6 +1020,8 @@ class SaoGetSupportLogUserDataListResponse(SaoBaseResponse): resp_data.support_log_user_data_list.append(support_data) + resp_data["support_log_user_data_list_size"] = len(resp_data.support_log_user_data_list) + resp_data = resp_struct.build(resp_data) self.length = len(resp_data) @@ -1061,6 +1069,8 @@ class SaoGetTitleUserDataListResponse(SaoBaseResponse): resp_data.title_user_data_list.append(title_data) + resp_data["title_user_data_list_size"] = len(resp_data.title_user_data_list) + # finally, rebuild the resp_data resp_data = resp_struct.build(resp_data) @@ -1120,6 +1130,8 @@ class SaoGetEpisodeAppendDataListResponse(SaoBaseResponse): own_num=self.own_num_list[i], )) + resp_data["episode_append_data_list_size"] = len(resp_data.episode_append_data_list) + # finally, rebuild the resp_data resp_data = resp_struct.build(resp_data) @@ -1857,6 +1869,10 @@ class SaoEpisodePlayEndUnanalyzedLogFixedResponse(SaoBaseResponse): resp_data.play_end_unanalyzed_log_reward_data_list.append(reward_resp_data) + resp_data["play_end_unanalyzed_log_reward_data_list_size"] = len(resp_data.play_end_unanalyzed_log_reward_data_list) + for i in range(len(resp_data.play_end_unanalyzed_log_reward_data_list)): + resp_data.play_end_unanalyzed_log_reward_data_list[i]["common_reward_data_size"] = len(resp_data.play_end_unanalyzed_log_reward_data_list[i]["common_reward_data"]) + # finally, rebuild the resp_data resp_data = resp_struct.build(resp_data) @@ -1871,6 +1887,7 @@ class SaoGetQuestSceneUserDataListRequest(SaoBaseRequest): class SaoGetQuestSceneUserDataListResponse(SaoBaseResponse): def __init__(self, cmd, quest_data) -> None: super().__init__(cmd) + self.length = None self.result = 1 # quest_scene_user_data_list_size @@ -1967,6 +1984,10 @@ class SaoGetQuestSceneUserDataListResponse(SaoBaseResponse): resp_data.quest_scene_user_data_list.append(quest_resp_data) + resp_data["quest_scene_user_data_list_size"] = len(resp_data.quest_scene_user_data_list) + for i in range(len(resp_data.quest_scene_user_data_list)): + resp_data.quest_scene_user_data_list[i]["quest_scene_best_score_user_data_size"] = len(resp_data.quest_scene_user_data_list[i]["quest_scene_best_score_user_data"]) + # finally, rebuild the resp_data resp_data = resp_struct.build(resp_data) @@ -2252,6 +2273,8 @@ class SaoSynthesizeEnhancementHeroLogResponse(SaoBaseResponse): resp_data.hero_log_user_data_list.append(hero_data) + resp_data["hero_log_user_data_list_size"] = len(resp_data.hero_log_user_data_list) + # finally, rebuild the resp_data resp_data = resp_struct.build(resp_data) @@ -2411,6 +2434,8 @@ class SaoSynthesizeEnhancementEquipmentResponse(SaoBaseResponse): resp_data.after_equipment_user_data.append(synthesize_equipment_data) + resp_data["after_equipment_user_data_size"] = len(resp_data.after_equipment_user_data) + # finally, rebuild the resp_data resp_data = resp_struct.build(resp_data) @@ -2911,6 +2936,8 @@ class SaoScanQrQuestProfileCardResponse(SaoBaseResponse): resp_data.read_profile_card_data.append(hero_data) + resp_data["read_profile_card_data_size"] = len(resp_data.read_profile_card_data) + # finally, rebuild the resp_data resp_data = resp_struct.build(resp_data) From ff48438b85ea839dfef385fe5c310ef914cff1c2 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 12 Nov 2023 17:26:21 -0500 Subject: [PATCH 048/101] sao: fix change_party --- titles/sao/base.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/titles/sao/base.py b/titles/sao/base.py index e1c73ef..a73cb25 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -354,7 +354,7 @@ class SaoBase: party_hero_list = [] for party_team in req_data.party_data_list[0].party_team_data_list: - hero_data = self.game_data.item.get_hero_log(req_data.user_id, party_team["user_hero_log_id"]) + hero_data = self.game_data.item.get_hero_log(req_data.user_id, party_team.user_hero_log_id) hero_level = 1 hero_exp = 0 @@ -364,19 +364,19 @@ class SaoBase: self.game_data.item.put_hero_log( req_data.user_id, - party_team["user_hero_log_id"], + party_team.user_hero_log_id, hero_level, hero_exp, - party_team["main_weapon_user_equipment_id"], - party_team["sub_equipment_user_equipment_id"], - party_team["skill_slot1_skill_id"], - party_team["skill_slot2_skill_id"], - party_team["skill_slot3_skill_id"], - party_team["skill_slot4_skill_id"], - party_team["skill_slot5_skill_id"] + party_team.main_weapon_user_equipment_id, + party_team.sub_equipment_user_equipment_id, + party_team.skill_slot1_skill_id, + party_team.skill_slot2_skill_id, + party_team.skill_slot3_skill_id, + party_team.skill_slot4_skill_id, + party_team.skill_slot5_skill_id ) - party_hero_list.append(party_team["user_hero_log_id"]) + party_hero_list.append(party_team.user_hero_log_id) self.game_data.item.put_hero_party(req_data.user_id, req_data.party_data_list[0].party_team_data_list[0].user_party_team_id, party_hero_list[0], party_hero_list[1], party_hero_list[2]) From 9026c25fcc7d18dcc6d29ef0eeee35de0ddcbe0f Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 12 Nov 2023 17:35:06 -0500 Subject: [PATCH 049/101] sao: fix both unanalyzed log fixed requests --- titles/sao/base.py | 30 ++++-------------------------- titles/sao/handlers/base.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 26 deletions(-) diff --git a/titles/sao/base.py b/titles/sao/base.py index a73cb25..b7d814e 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -874,40 +874,18 @@ class SaoBase: def handle_c90a(self, header: SaoRequestHeader, request: bytes) -> bytes: #quest/episode_play_end_unanalyzed_log_fixed - req = bytes.fromhex(request)[24:] + req = SaoEpisodePlayEndUnanalyzedLogFixedRequest(header, request) - req_struct = Struct( - Padding(16), - "ticket_id_size" / Rebuild(Int32ub, len_(this.ticket_id) * 2), # calculates the length of the ticket_id - "ticket_id" / PaddedString(this.ticket_id_size, "utf_16_le"), # ticket_id is a (zero) padded string - "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id - "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string - ) - - req_data = req_struct.parse(req) - user_id = req_data.user_id - - end_session_data = self.game_data.item.get_end_session(user_id) + end_session_data = self.game_data.item.get_end_session(req.user_id) resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(header.cmd +1, end_session_data[4]) return resp.make() def handle_c91a(self, header: SaoRequestHeader, request: bytes) -> bytes: # handler is identical to the episode #quest/trial_tower_play_end_unanalyzed_log_fixed - req = bytes.fromhex(request)[24:] + req = TrialTowerPlayEndUnanalyzedLogFixed(header, request) - req_struct = Struct( - Padding(16), - "ticket_id_size" / Rebuild(Int32ub, len_(this.ticket_id) * 2), # calculates the length of the ticket_id - "ticket_id" / PaddedString(this.ticket_id_size, "utf_16_le"), # ticket_id is a (zero) padded string - "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id - "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string - ) - - req_data = req_struct.parse(req) - user_id = req_data.user_id - - end_session_data = self.game_data.item.get_end_session(user_id) + end_session_data = self.game_data.item.get_end_session(req.user_id) resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(header.cmd +1, end_session_data[4]) return resp.make() diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py index db6e98f..836372b 100644 --- a/titles/sao/handlers/base.py +++ b/titles/sao/handlers/base.py @@ -1798,6 +1798,20 @@ class SaoTrialTowerPlayEndResponse(SaoBaseResponse): class SaoEpisodePlayEndUnanalyzedLogFixedRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) + off = 0 + ticket_id = decode_str(data, off) + self.ticket_id = ticket_id[0] + off += ticket_id[1] + + user_id = decode_str(data, off) + self.user_id = user_id[0] + off += user_id[1] + + self.episode_id = decode_int(data, off) + off += INT_OFF + + self.rarity_up_exec_flag = decode_byte(data, off) + off += BYTE_OFF class SaoEpisodePlayEndUnanalyzedLogFixedResponse(SaoBaseResponse): def __init__(self, cmd, end_session_data) -> None: @@ -2989,3 +3003,21 @@ class SaoChangePartyRequest(SaoBaseRequest): tmp = PartyData(data, off) self.party_data_list.append(tmp) off += tmp.get_size() + +class TrialTowerPlayEndUnanalyzedLogFixed(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + ticket_id = decode_str(data, off) + self.ticket_id = ticket_id[0] + off += ticket_id[1] + + user_id = decode_str(data, off) + self.user_id = user_id[0] + off += user_id[1] + + self.trial_tower_id = decode_int(data, off) + off += INT_OFF + + self.rarity_up_exec_flag = decode_byte(data, off) + off += BYTE_OFF From 9bc76279f1c6867b554d8ab42486ef86f13812cd Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 12 Nov 2023 18:46:38 -0500 Subject: [PATCH 050/101] sao: fix episode_play_end --- titles/sao/base.py | 25 +--------------- titles/sao/handlers/base.py | 45 +++++++++++++++++++++++++++++ titles/sao/handlers/helpers.py | 53 +++++++++++++++++++++++++++------- 3 files changed, 89 insertions(+), 34 deletions(-) diff --git a/titles/sao/base.py b/titles/sao/base.py index b7d814e..facae40 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -563,30 +563,7 @@ class SaoBase: def handle_c914(self, header: SaoRequestHeader, request: bytes) -> bytes: #quest/trial_tower_play_start - req = bytes.fromhex(request)[24:] - - req_struct = Struct( - Padding(16), - "ticket_id_size" / Rebuild(Int32ub, len_(this.ticket_id) * 2), # calculates the length of the ticket_id - "ticket_id" / PaddedString(this.ticket_id_size, "utf_16_le"), # ticket_id is a (zero) padded string - "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id - "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string - "trial_tower_id" / Int32ub, # trial_tower_id is an int - "play_mode" / Int8ub, # play_mode is a byte - Padding(3), - "play_start_request_data_length" / Rebuild(Int8ub, len_(this.play_start_request_data)), # play_start_request_data_length is a byte, - "play_start_request_data" / Array(this.play_start_request_data_length, Struct( - "user_party_id_size" / Rebuild(Int32ub, len_(this.user_party_id) * 2), # calculates the length of the user_party_id - "user_party_id" / PaddedString(this.user_party_id_size, "utf_16_le"), # user_party_id is a (zero) padded string - "appoint_leader_resource_card_code_size" / Rebuild(Int32ub, len_(this.appoint_leader_resource_card_code) * 2), # calculates the length of the total_damage - "appoint_leader_resource_card_code" / PaddedString(this.appoint_leader_resource_card_code_size, "utf_16_le"), # total_damage is a (zero) padded string - "use_profile_card_code_size" / Rebuild(Int32ub, len_(this.use_profile_card_code) * 2), # calculates the length of the total_damage - "use_profile_card_code" / PaddedString(this.use_profile_card_code_size, "utf_16_le"), # use_profile_card_code is a (zero) padded string - "quest_drop_boost_apply_flag" / Int8ub, # quest_drop_boost_apply_flag is a byte - )), - ) - - req_data = req_struct.parse(req) + req_data = SaoTrialTowerPlayStartRequest(header, request) user_id = req_data.user_id floor_id = req_data.trial_tower_id diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py index 836372b..c1453d0 100644 --- a/titles/sao/handlers/base.py +++ b/titles/sao/handlers/base.py @@ -1508,6 +1508,15 @@ class SaoEpisodePlayStartRequest(SaoBaseRequest): tmp = PlayStartRequestData(data, off) self.play_start_request_data.append(tmp) off += tmp.get_size() + + self.multi_play_start_request_data_count = decode_int(data, off) + off += INT_OFF + + self.multi_play_start_request_data: List[MultiPlayStartRequestData] = [] + for _ in range(self.multi_play_start_request_data_count): + tmp = MultiPlayStartRequestData(data, off) + off += tmp.get_size() + self.multi_play_start_request_data.append(tmp) class SaoEpisodePlayStartResponse(SaoBaseResponse): def __init__(self, cmd, profile_data) -> None: @@ -1669,6 +1678,42 @@ class SaoEpisodePlayEndResponse(SaoBaseResponse): self.length = len(resp_data) return super().make() + resp_data +class SaoTrialTowerPlayStartRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + ticket_id = decode_str(data, off) + self.ticket_id = ticket_id[0] + off += ticket_id[1] + + user_id = decode_str(data, off) + self.user_id = user_id[0] + off += user_id[1] + + self.trial_tower_id = decode_int(data, off) + off += INT_OFF + + self.play_mode = decode_byte(data, off) + off += BYTE_OFF + + self.play_start_request_data_count = decode_int(data, off) + off += INT_OFF + + self.play_start_request_data: List[PlayStartRequestData] = [] + for _ in range(self.play_start_request_data_count): + tmp = PlayStartRequestData(data, off) + self.play_start_request_data.append(tmp) + off += tmp.get_size() + + self.multi_play_start_request_data_count = decode_int(data, off) + off += INT_OFF + + self.multi_play_start_request_data: List[MultiPlayStartRequestData] = [] + for _ in range(self.multi_play_start_request_data_count): + tmp = MultiPlayStartRequestData(data, off) + off += tmp.get_size() + self.multi_play_start_request_data.append(tmp) + class SaoTrialTowerPlayEndRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) diff --git a/titles/sao/handlers/helpers.py b/titles/sao/handlers/helpers.py index 9c5f25a..8706ca9 100644 --- a/titles/sao/handlers/helpers.py +++ b/titles/sao/handlers/helpers.py @@ -283,7 +283,7 @@ class PlayEndRequestData(BaseHelper): self.base_get_data_list: List[BaseGetData] = [] for _ in range(self.base_get_data_count): - tmp = BaseGetData(data, sz) + tmp = BaseGetData(data, offset + sz) sz += tmp.get_size() self.base_get_data_list.append(tmp) @@ -292,7 +292,7 @@ class PlayEndRequestData(BaseHelper): self.get_player_trace_data: List[GetPlayerTraceData] = [] for _ in range(self.get_player_trace_data_count): - tmp = GetPlayerTraceData(data, sz) + tmp = GetPlayerTraceData(data, offset + sz) sz += tmp.get_size() self.get_player_trace_data.append(tmp) @@ -301,7 +301,7 @@ class PlayEndRequestData(BaseHelper): self.get_rare_drop_data_list: List[RareDropData] = [] for _ in range(self.get_rare_drop_data_count): - tmp = RareDropData(data, sz) + tmp = RareDropData(data, offset + sz) sz += tmp.get_size() self.get_rare_drop_data_list.append(tmp) @@ -310,7 +310,7 @@ class PlayEndRequestData(BaseHelper): self.get_special_rare_drop_data_list: List[SpecialRareDropData] = [] for _ in range(self.get_special_rare_drop_data_count): - tmp = SpecialRareDropData(data, sz) + tmp = SpecialRareDropData(data, offset + sz) sz += tmp.get_size() self.get_special_rare_drop_data_list.append(tmp) @@ -319,7 +319,7 @@ class PlayEndRequestData(BaseHelper): self.get_unanalyzed_log_tmp_reward_data_list: List[UnanalyzedLogTmpRewardData] = [] for _ in range(self.get_unanalyzed_log_tmp_reward_data_count): - tmp = UnanalyzedLogTmpRewardData(data, sz) + tmp = UnanalyzedLogTmpRewardData(data, offset + sz) sz += tmp.get_size() self.get_unanalyzed_log_tmp_reward_data_list.append(tmp) @@ -328,7 +328,7 @@ class PlayEndRequestData(BaseHelper): self.get_event_item_data_list: List[EventItemData] = [] for _ in range(self.get_event_item_data_count): - tmp = EventItemData(data, sz) + tmp = EventItemData(data, offset + sz) sz += tmp.get_size() self.get_event_item_data_list.append(tmp) @@ -337,7 +337,7 @@ class PlayEndRequestData(BaseHelper): self.discovery_enemy_data_list: List[DiscoveryEnemyData] = [] for _ in range(self.discovery_enemy_data_count): - tmp = DiscoveryEnemyData(data, sz) + tmp = DiscoveryEnemyData(data, offset + sz) sz += tmp.get_size() self.discovery_enemy_data_list.append(tmp) @@ -346,7 +346,7 @@ class PlayEndRequestData(BaseHelper): self.destroy_boss_data_list: List[DestroyBossData] = [] for _ in range(self.destroy_boss_data_count): - tmp = DestroyBossData(data, sz) + tmp = DestroyBossData(data, offset + sz) sz += tmp.get_size() self.destroy_boss_data_list.append(tmp) @@ -355,7 +355,7 @@ class PlayEndRequestData(BaseHelper): self.mission_data_list: List[MissionData] = [] for _ in range(self.mission_data_count): - tmp = MissionData(data, sz) + tmp = MissionData(data, offset + sz) sz += tmp.get_size() self.mission_data_list.append(tmp) @@ -364,12 +364,45 @@ class PlayEndRequestData(BaseHelper): self.score_data_list: List[ScoreData] = [] for _ in range(self.score_data_count): - tmp = ScoreData(data, sz) + tmp = ScoreData(data, offset + sz) sz += tmp.get_size() self.score_data_list.append(tmp) self._sz = sz +class EntryUserData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + store_id = decode_str(data, offset + self._sz) + self.store_id = store_id[0] + self._sz += store_id[1] + + user_id = decode_str(data, offset + self._sz) + self.user_id = user_id[0] + self._sz += user_id[1] + + self.host_flag = decode_byte(data, offset + self._sz) + self._sz += BYTE_OFF + +class MultiPlayStartRequestData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + room_id = decode_str(data, offset + self._sz) + self.room_id = room_id[0] + self._sz += room_id[1] + + self.matching_mode = decode_byte(data, offset + self._sz) + self._sz += BYTE_OFF + + self.entry_user_data_count = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + self.entry_user_data_list: List[EntryUserData] = [] + for _ in range(self.entry_user_data_count): + tmp = EntryUserData(data, offset + self._sz) + self._sz += tmp.get_size() + self.entry_user_data_list.append(tmp) + class MultiPlayEndRequestData(BaseHelper): def __init__(self, data: bytes, offset: int) -> None: super().__init__(data, offset) From c384a8d1f55ce85dcc73b1b70512bffcf0319d83 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 12 Nov 2023 19:22:46 -0500 Subject: [PATCH 051/101] sao: properly initialize material_common_reward_user_data_count --- titles/sao/handlers/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py index c1453d0..ab19243 100644 --- a/titles/sao/handlers/base.py +++ b/titles/sao/handlers/base.py @@ -2356,7 +2356,7 @@ class SaoSynthesizeEnhancementEquipmentRequest(SaoBaseRequest): self.origin_user_equipment_id = origin_user_equipment_id[0] off += origin_user_equipment_id[1] - self.material_common_reward_user_data_list: List[MaterialCommonRewardUserData] + self.material_common_reward_user_data_list: List[MaterialCommonRewardUserData] = [] self.material_common_reward_user_data_count = decode_int(data, off) off += INT_OFF From ab3b6a98143882948424e8334941263adcd82a45 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 12 Nov 2023 19:24:08 -0500 Subject: [PATCH 052/101] sao: fix SaoSynthesizeEnhancementHeroLogRequest --- titles/sao/handlers/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py index ab19243..fc13faf 100644 --- a/titles/sao/handlers/base.py +++ b/titles/sao/handlers/base.py @@ -2177,7 +2177,7 @@ class SaoSynthesizeEnhancementHeroLogRequest(SaoBaseRequest): self.origin_user_hero_log_id = origin_user_hero_log_id[0] off += origin_user_hero_log_id[1] - self.material_common_reward_user_data_list: List[MaterialCommonRewardUserData] + self.material_common_reward_user_data_list: List[MaterialCommonRewardUserData] = [] self.material_common_reward_user_data_count = decode_int(data, off) off += INT_OFF From 3596f0f34c21154e15d931a9317f7bbf5f81a492 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 12 Nov 2023 19:45:39 -0500 Subject: [PATCH 053/101] sao: fix synthesize_enhancement_equipment --- titles/sao/base.py | 24 ++++++++++++++---------- titles/sao/handlers/helpers.py | 11 ++++++----- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/titles/sao/base.py b/titles/sao/base.py index facae40..53df0d2 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -303,26 +303,30 @@ class SaoBase: req_data = SaoSynthesizeEnhancementEquipmentRequest(header, request) synthesize_equipment_data = self.game_data.item.get_user_equipment(req_data.user_id, req_data.origin_user_equipment_id) - for i in range(0,req_data.material_common_reward_user_data_count): - - itemList = self.game_data.static.get_item_id(req_data.material_common_reward_user_data_list[i].user_common_reward_id) - heroList = self.game_data.static.get_hero_id(req_data.material_common_reward_user_data_list[i].user_common_reward_id) - equipmentList = self.game_data.static.get_equipment_id(req_data.material_common_reward_user_data_list[i].user_common_reward_id) + for x in req_data.material_common_reward_user_data_list: + equipment_exp = 0 + itemList = self.game_data.static.get_item_id(x.user_common_reward_id) + heroList = self.game_data.static.get_hero_id(x.user_common_reward_id) + equipmentList = self.game_data.static.get_equipment_id(x.user_common_reward_id) if itemList: equipment_exp = 2000 + int(synthesize_equipment_data["enhancement_exp"]) - self.game_data.item.remove_item(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id) + self.game_data.item.remove_item(req_data.user_id, x.user_common_reward_id) if equipmentList: - equipment_data = self.game_data.item.get_user_equipment(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id) + equipment_data = self.game_data.item.get_user_equipment(req_data.user_id, x.user_common_reward_id) equipment_exp = int(equipment_data["enhancement_exp"]) + int(synthesize_equipment_data["enhancement_exp"]) - self.game_data.item.remove_equipment(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id) + self.game_data.item.remove_equipment(req_data.user_id, x.user_common_reward_id) if heroList: - hero_data = self.game_data.item.get_hero_log(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id) + hero_data = self.game_data.item.get_hero_log(req_data.user_id, x.user_common_reward_id) equipment_exp = int(hero_data["log_exp"]) + int(synthesize_equipment_data["enhancement_exp"]) - self.game_data.item.remove_hero_log(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id) + self.game_data.item.remove_hero_log(req_data.user_id, x.user_common_reward_id) + if equipment_exp == 0: + self.logger.warn(f"Common reward {x.user_common_reward_id} (type {x.common_reward_type}) not found!") + continue + self.game_data.item.put_equipment_data(req_data.user_id, int(req_data.origin_user_equipment_id), synthesize_equipment_data["enhancement_value"], equipment_exp, 0, 0, 0) profile = self.game_data.profile.get_profile(req_data.user_id) diff --git a/titles/sao/handlers/helpers.py b/titles/sao/handlers/helpers.py index 8706ca9..ba91751 100644 --- a/titles/sao/handlers/helpers.py +++ b/titles/sao/handlers/helpers.py @@ -73,12 +73,13 @@ class BaseHelper: class MaterialCommonRewardUserData(BaseHelper): def __init__(self, data: bytes, offset: int) -> None: - self.common_reward_type = decode_short(data, offset) - offset += SHORT_OFF + super().__init__(data, offset) + self.common_reward_type = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF - self.user_common_reward_id = decode_short(data, offset) - - self._sz = SHORT_OFF + SHORT_OFF + user_common_reward_id = decode_str(data, offset + self._sz) + self.user_common_reward_id = user_common_reward_id[0] + self._sz += user_common_reward_id[1] class PartyTeamData(BaseHelper): def __init__(self, data: bytes, offset: int) -> None: From a76764636147363eee9345cb47d44391935a0fca Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 12 Nov 2023 19:48:03 -0500 Subject: [PATCH 054/101] sao: make synthesize_enhancement_hero_log similar to synthesize_enhancement_equipment --- titles/sao/base.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/titles/sao/base.py b/titles/sao/base.py index 53df0d2..16926d8 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -241,25 +241,28 @@ class SaoBase: synthesize_hero_log_data = self.game_data.item.get_hero_log(req.user_id, req.origin_user_hero_log_id) - for i in range(req.material_common_reward_user_data_count): - - itemList = self.game_data.static.get_item_id(req.material_common_reward_user_data_list[i].user_common_reward_id) - heroList = self.game_data.static.get_hero_id(req.material_common_reward_user_data_list[i].user_common_reward_id) - equipmentList = self.game_data.static.get_equipment_id(req.material_common_reward_user_data_list[i].user_common_reward_id) + for x in req.material_common_reward_user_data_list: + hero_exp = 0 + itemList = self.game_data.static.get_item_id(x.user_common_reward_id) + heroList = self.game_data.static.get_hero_id(x.user_common_reward_id) + equipmentList = self.game_data.static.get_equipment_id(x.user_common_reward_id) if itemList: hero_exp = 2000 + int(synthesize_hero_log_data["log_exp"]) - self.game_data.item.remove_item(req.user_id, req.material_common_reward_user_data_list[i].user_common_reward_id) + self.game_data.item.remove_item(req.user_id, x.user_common_reward_id) if equipmentList: - equipment_data = self.game_data.item.get_user_equipment(req.user_id, req.material_common_reward_user_data_list[i].user_common_reward_id) + equipment_data = self.game_data.item.get_user_equipment(req.user_id, x.user_common_reward_id) hero_exp = int(equipment_data["enhancement_exp"]) + int(synthesize_hero_log_data["log_exp"]) - self.game_data.item.remove_equipment(req.user_id, req.material_common_reward_user_data_list[i].user_common_reward_id) + self.game_data.item.remove_equipment(req.user_id, x.user_common_reward_id) if heroList: - hero_data = self.game_data.item.get_hero_log(req.user_id, req.material_common_reward_user_data_list[i].user_common_reward_id) + hero_data = self.game_data.item.get_hero_log(req.user_id, x.user_common_reward_id) hero_exp = int(hero_data["log_exp"]) + int(synthesize_hero_log_data["log_exp"]) - self.game_data.item.remove_hero_log(req.user_id, req.material_common_reward_user_data_list[i].user_common_reward_id) + self.game_data.item.remove_hero_log(req.user_id, x.user_common_reward_id) + + if hero_exp == 0: + self.logger.warn(f"Hero {x.user_common_reward_id} (type {x.common_reward_type}) not found!") self.game_data.item.put_hero_log( req.user_id, From b516da2e6e2d0faa5d2e8d63ff1d879a4893520f Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 12 Nov 2023 19:53:36 -0500 Subject: [PATCH 055/101] sao: add logging to synthesize_enhancement_hero_log --- titles/sao/base.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/titles/sao/base.py b/titles/sao/base.py index 16926d8..628478b 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -253,11 +253,19 @@ class SaoBase: if equipmentList: equipment_data = self.game_data.item.get_user_equipment(req.user_id, x.user_common_reward_id) + if equipment_data is None: + self.logger.error(f"Failed to find equipment {x.user_common_reward_id} for user {req.user_id}!") + continue + hero_exp = int(equipment_data["enhancement_exp"]) + int(synthesize_hero_log_data["log_exp"]) self.game_data.item.remove_equipment(req.user_id, x.user_common_reward_id) if heroList: hero_data = self.game_data.item.get_hero_log(req.user_id, x.user_common_reward_id) + if hero_data is None: + self.logger.error(f"Failed to find hero {x.user_common_reward_id} for user {req.user_id}!") + continue + hero_exp = int(hero_data["log_exp"]) + int(synthesize_hero_log_data["log_exp"]) self.game_data.item.remove_hero_log(req.user_id, x.user_common_reward_id) From 84b44d2a02c86077cf2df7a45026df83c0ab74ab Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 12 Nov 2023 19:55:10 -0500 Subject: [PATCH 056/101] sao: add logging to synthesize_enhancement_equipment --- titles/sao/base.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/titles/sao/base.py b/titles/sao/base.py index 628478b..4e520c7 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -326,11 +326,19 @@ class SaoBase: if equipmentList: equipment_data = self.game_data.item.get_user_equipment(req_data.user_id, x.user_common_reward_id) + if equipment_data is None: + self.logger.error(f"Failed to find equipment {x.user_common_reward_id} for user {req_data.user_id}!") + continue + equipment_exp = int(equipment_data["enhancement_exp"]) + int(synthesize_equipment_data["enhancement_exp"]) self.game_data.item.remove_equipment(req_data.user_id, x.user_common_reward_id) if heroList: hero_data = self.game_data.item.get_hero_log(req_data.user_id, x.user_common_reward_id) + if hero_data is None: + self.logger.error(f"Failed to find hero {x.user_common_reward_id} for user {req_data.user_id}!") + continue + equipment_exp = int(hero_data["log_exp"]) + int(synthesize_equipment_data["enhancement_exp"]) self.game_data.item.remove_hero_log(req_data.user_id, x.user_common_reward_id) From 4bedf71d3d6283628011ac08dabd66a9e4a67829 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 12 Nov 2023 20:02:08 -0500 Subject: [PATCH 057/101] sao: fix trial_tower_play_end --- titles/sao/base.py | 111 ++++-------------------------------- titles/sao/handlers/base.py | 29 ++++++++++ 2 files changed, 41 insertions(+), 99 deletions(-) diff --git a/titles/sao/base.py b/titles/sao/base.py index 4e520c7..91bdd1a 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -605,104 +605,17 @@ class SaoBase: def handle_c918(self, header: SaoRequestHeader, request: bytes) -> bytes: #quest/trial_tower_play_end - req = bytes.fromhex(request)[24:] - - req_struct = Struct( - Padding(20), - "ticket_id" / Bytes(1), # needs to be parsed as an int - Padding(1), - "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id - "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string - Padding(2), - "trial_tower_id" / Int16ub, # trial_tower_id is a short, - Padding(3), - "play_end_request_data" / Int8ub, # play_end_request_data is a byte - Padding(1), - "play_result_flag" / Int8ub, # play_result_flag is a byte - Padding(2), - "base_get_data_length" / Rebuild(Int8ub, len_(this.base_get_data)), # base_get_data_length is a byte, - "base_get_data" / Array(this.base_get_data_length, Struct( - "get_hero_log_exp" / Int32ub, # get_hero_log_exp is an int - "get_col" / Int32ub, # get_num is a short - )), - Padding(3), - "get_player_trace_data_list_length" / Rebuild(Int8ub, len_(this.get_player_trace_data_list)), # get_player_trace_data_list_length is a byte - "get_player_trace_data_list" / Array(this.get_player_trace_data_list_length, Struct( - "user_quest_scene_player_trace_id" / Int32ub, # user_quest_scene_player_trace_id is an int - )), - Padding(3), - "get_rare_drop_data_list_length" / Rebuild(Int8ub, len_(this.get_rare_drop_data_list)), # get_rare_drop_data_list_length is a byte - "get_rare_drop_data_list" / Array(this.get_rare_drop_data_list_length, Struct( - "quest_rare_drop_id" / Int32ub, # quest_rare_drop_id is an int - )), - Padding(3), - "get_special_rare_drop_data_list_length" / Rebuild(Int8ub, len_(this.get_special_rare_drop_data_list)), # get_special_rare_drop_data_list_length is a byte - "get_special_rare_drop_data_list" / Array(this.get_special_rare_drop_data_list_length, Struct( - "quest_special_rare_drop_id" / Int32ub, # quest_special_rare_drop_id is an int - )), - Padding(3), - "get_unanalyzed_log_tmp_reward_data_list_length" / Rebuild(Int8ub, len_(this.get_unanalyzed_log_tmp_reward_data_list)), # get_unanalyzed_log_tmp_reward_data_list_length is a byte - "get_unanalyzed_log_tmp_reward_data_list" / Array(this.get_unanalyzed_log_tmp_reward_data_list_length, Struct( - "unanalyzed_log_grade_id" / Int32ub, # unanalyzed_log_grade_id is an int, - )), - Padding(3), - "get_event_item_data_list_length" / Rebuild(Int8ub, len_(this.get_event_item_data_list)), # get_event_item_data_list_length is a byte, - "get_event_item_data_list" / Array(this.get_event_item_data_list_length, Struct( - "event_item_id" / Int32ub, # event_item_id is an int - "get_num" / Int16ub, # get_num is a short - )), - Padding(3), - "discovery_enemy_data_list_length" / Rebuild(Int8ub, len_(this.discovery_enemy_data_list)), # discovery_enemy_data_list_length is a byte - "discovery_enemy_data_list" / Array(this.discovery_enemy_data_list_length, Struct( - "enemy_kind_id" / Int32ub, # enemy_kind_id is an int - "destroy_num" / Int16ub, # destroy_num is a short - )), - Padding(3), - "destroy_boss_data_list_length" / Rebuild(Int8ub, len_(this.destroy_boss_data_list)), # destroy_boss_data_list_length is a byte - "destroy_boss_data_list" / Array(this.destroy_boss_data_list_length, Struct( - "boss_type" / Int8ub, # boss_type is a byte - "enemy_kind_id" / Int32ub, # enemy_kind_id is an int - "destroy_num" / Int16ub, # destroy_num is a short - )), - Padding(3), - "mission_data_list_length" / Rebuild(Int8ub, len_(this.mission_data_list)), # mission_data_list_length is a byte - "mission_data_list" / Array(this.mission_data_list_length, Struct( - "mission_id" / Int32ub, # enemy_kind_id is an int - "clear_flag" / Int8ub, # boss_type is a byte - "mission_difficulty_id" / Int16ub, # destroy_num is a short - )), - Padding(3), - "score_data_length" / Rebuild(Int8ub, len_(this.score_data)), # score_data_length is a byte - "score_data" / Array(this.score_data_length, Struct( - "clear_time" / Int32ub, # clear_time is an int - "combo_num" / Int32ub, # boss_type is a int - "total_damage_size" / Rebuild(Int32ub, len_(this.total_damage) * 2), # calculates the length of the total_damage - "total_damage" / PaddedString(this.total_damage_size, "utf_16_le"), # total_damage is a (zero) padded string - "concurrent_destroying_num" / Int16ub, # concurrent_destroying_num is a short - "reaching_skill_level" / Int16ub, # reaching_skill_level is a short - "ko_chara_num" / Int8ub, # ko_chara_num is a byte - "acceleration_invocation_num" / Int16ub, # acceleration_invocation_num is a short - "boss_destroying_num" / Int16ub, # boss_destroying_num is a short - "synchro_skill_used_flag" / Int8ub, # synchro_skill_used_flag is a byte - "used_friend_skill_id" / Int32ub, # used_friend_skill_id is an int - "friend_skill_used_flag" / Int8ub, # friend_skill_used_flag is a byte - "continue_cnt" / Int16ub, # continue_cnt is a short - "total_loss_num" / Int16ub, # total_loss_num is a short - )), - - ) - - req_data = req_struct.parse(req) + req_data = SaoTrialTowerPlayEndRequest(header, request) # Add tower progression to database user_id = req_data.user_id trial_tower_id = req_data.trial_tower_id next_tower_id = 0 - quest_clear_flag = bool(req_data.score_data[0].boss_destroying_num) - clear_time = req_data.score_data[0].clear_time - combo_num = req_data.score_data[0].combo_num - total_damage = req_data.score_data[0].total_damage - concurrent_destroying_num = req_data.score_data[0].concurrent_destroying_num + quest_clear_flag = bool(req_data.play_end_request_data_list[0].score_data_list[0].boss_destroying_num) + clear_time = req_data.play_end_request_data_list[0].score_data_list[0].clear_time + combo_num = req_data.play_end_request_data_list[0].score_data_list[0].combo_num + total_damage = req_data.play_end_request_data_list[0].score_data_list[0].total_damage + concurrent_destroying_num = req_data.play_end_request_data_list[0].score_data_list[0].concurrent_destroying_num if quest_clear_flag is True: # Save tower progression - to be revised to avoid saving worse score @@ -747,7 +660,7 @@ class SaoBase: profile = self.game_data.profile.get_profile(user_id) exp = int(profile["rank_exp"]) + 100 #always 100 extra exp for some reason - col = int(profile["own_col"]) + int(req_data.base_get_data[0].get_col) + col = int(profile["own_col"]) + int(req_data.play_end_request_data_list[0].base_get_data_list[0].get_col) # Calculate level based off experience and the CSV list with open(r'titles/sao/data/PlayerRank.csv') as csv_file: @@ -790,7 +703,7 @@ class SaoBase: for i in range(0,len(hero_list)): hero_data = self.game_data.item.get_hero_log(user_id, hero_list[i]) - log_exp = int(hero_data["log_exp"]) + int(req_data.base_get_data[0].get_hero_log_exp) + log_exp = int(hero_data["log_exp"]) + int(req_data.play_end_request_data_list[0].base_get_data_list[0].get_hero_log_exp) # Calculate hero level based off experience and the CSV list with open(r'titles/sao/data/HeroLogLevel.csv') as csv_file: @@ -826,8 +739,8 @@ class SaoBase: json_data = {"data": []} # Grab the rare loot from the table, match it with the right item and then push to the player profile - for r in range(0,req_data.get_rare_drop_data_list_length): - rewardList = self.game_data.static.get_rare_drop_id(int(req_data.get_rare_drop_data_list[r].quest_rare_drop_id)) + for x in req_data.play_end_request_data_list[0].get_rare_drop_data_list: + rewardList = self.game_data.static.get_rare_drop_id(int(x.quest_rare_drop_id)) commonRewardId = rewardList["commonRewardId"] heroList = self.game_data.static.get_hero_id(commonRewardId) @@ -842,14 +755,14 @@ class SaoBase: self.game_data.item.put_item(user_id, commonRewardId) # Generate random hero(es) based off the response - for a in range(0,req_data.get_unanalyzed_log_tmp_reward_data_list_length): + for x in req_data.play_end_request_data_list[0].get_unanalyzed_log_tmp_reward_data_list: with open('titles/sao/data/RewardTable.csv', 'r') as f: keys_unanalyzed = next(f).strip().split(',') data_unanalyzed = list(DictReader(f, fieldnames=keys_unanalyzed)) randomized_unanalyzed_id = choice(data_unanalyzed) - while int(randomized_unanalyzed_id['UnanalyzedLogGradeId']) != req_data.get_unanalyzed_log_tmp_reward_data_list[a].unanalyzed_log_grade_id: + while int(randomized_unanalyzed_id['UnanalyzedLogGradeId']) != x.unanalyzed_log_grade_id: randomized_unanalyzed_id = choice(data_unanalyzed) heroList = self.game_data.static.get_hero_id(randomized_unanalyzed_id['CommonRewardId']) diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py index fc13faf..3c20e80 100644 --- a/titles/sao/handlers/base.py +++ b/titles/sao/handlers/base.py @@ -1717,6 +1717,35 @@ class SaoTrialTowerPlayStartRequest(SaoBaseRequest): class SaoTrialTowerPlayEndRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) + off = 0 + ticket_id = decode_str(data, off) + self.ticket_id = ticket_id[0] + off += ticket_id[1] + + user_id = decode_str(data, off) + self.user_id = user_id[0] + off += user_id[1] + + self.trial_tower_id = decode_int(data, off) + off += INT_OFF + + self.play_end_request_data_count = decode_int(data, off) + off += INT_OFF + + self.play_end_request_data_list: List[PlayEndRequestData] = [] + for _ in range(self.play_end_request_data_count): + tmp = PlayEndRequestData(data, off) + off += tmp.get_size() + self.play_end_request_data_list.append(tmp) + + self.multi_play_end_request_data_count = decode_int(data, off) + off += INT_OFF + + self.multi_play_end_request_data_list: List[MultiPlayEndRequestData] = [] + for _ in range(self.multi_play_end_request_data_count): + tmp = MultiPlayEndRequestData(data, off) + off += tmp.get_size() + self.multi_play_end_request_data_list.append(tmp) class SaoTrialTowerPlayEndResponse(SaoBaseResponse): def __init__(self, cmd) -> None: From 4da886a083c9e9a7ab384eb966d9ebb1c6cfcd6b Mon Sep 17 00:00:00 2001 From: phantomlan Date: Sun, 5 Nov 2023 18:09:58 +0100 Subject: [PATCH 058/101] - 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 --- docs/game_specific_info.md | 38 +++++++++++++++++- titles/ongeki/base.py | 75 ++++++++++++++++++++++++++--------- titles/ongeki/const.py | 3 +- titles/ongeki/schema/item.py | 76 ++++++++++++++++++++++++++++++++++++ 4 files changed, 171 insertions(+), 21 deletions(-) diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index 9f39a7e..c338279 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -336,6 +336,42 @@ perform all previous updates as well: 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_tech_music_list` +``` +id: Id in table, just increment for each entry +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) + + + + ## Card Maker ### SDED @@ -587,4 +623,4 @@ python dbutils.py --game SDEW upgrade - Midorica - Limited Network Support - Dniel97 - Helping with network base -- tungnotpunk - Source \ No newline at end of file +- tungnotpunk - Source diff --git a/titles/ongeki/base.py b/titles/ongeki/base.py index c72c086..ab7c3ec 100644 --- a/titles/ongeki/base.py +++ b/titles/ongeki/base.py @@ -228,7 +228,21 @@ class OngekiBase: return {"length": 0, "gameSaleList": []} def handle_get_game_tech_music_api_request(self, data: Dict) -> Dict: - return {"length": 0, "gameTechMusicList": []} + music_list = self.data.item.get_tech_music() + + prep_music_list = [] + for music in music_list: + tmp = music._asdict() + tmp.pop("id") + prep_music_list.append(tmp) + + if prep_music_list is None: + return {"length": 0, "gameTechMusicList": []} + + return { + "length": len(prep_music_list), + "gameTechMusicList": prep_music_list, + } def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "UpsertClientSettingApi"} @@ -283,7 +297,7 @@ class OngekiBase: "endDate": "2099-12-31 00:00:00.0", } ) - + return { "type": data["type"], "length": len(evt_list), @@ -403,15 +417,24 @@ class OngekiBase: } def handle_get_user_tech_event_ranking_api_request(self, data: Dict) -> Dict: - # user_event_ranking_list = self.data.item.get_tech_event_ranking(data["userId"]) - # if user_event_ranking_list is None: return {} + user_tech_event_ranks = self.data.item.get_tech_event_ranking(data["userId"]) + if user_tech_event_ranks is None: + return { + "userId": data["userId"], + "length": 0, + "userTechEventRankingList": [], + } + # collect the whole table and clear other players, to preserve proper ranking evt_ranking = [] - # for evt in user_event_ranking_list: - # tmp = evt._asdict() - # tmp.pop("id") - # tmp.pop("user") - # evt_ranking.append(tmp) + for evt in user_tech_event_ranks: + tmp = evt._asdict() + if tmp["user"] != data["userId"]: + tmp.clear() + else: + tmp.pop("id") + tmp.pop("user") + evt_ranking.append(tmp) return { "userId": data["userId"], @@ -533,20 +556,26 @@ class OngekiBase: return {"userId": data["userId"], "userData": user_data} def handle_get_user_event_ranking_api_request(self, data: Dict) -> Dict: - # user_event_ranking_list = self.data.item.get_event_ranking(data["userId"]) - # if user_event_ranking_list is None: return {} + user_event_ranking_list = self.data.item.get_ranking_event_ranks(data["userId"]) + if user_event_ranking_list is None: + return {} - evt_ranking = [] - # for evt in user_event_ranking_list: - # tmp = evt._asdict() - # tmp.pop("id") - # tmp.pop("user") - # evt_ranking.append(tmp) + # We collect the whole ranking table, and clear out any not needed data, this way we preserve the proper ranking + # In official spec this should be done server side, in maintenance period + prep_event_ranking = [] + for evt in user_event_ranking_list: + tmp = evt._asdict() + if tmp["user"] != data["userId"]: + tmp.clear() + else: + tmp.pop("id") + tmp.pop("user") + prep_event_ranking.append(tmp) return { "userId": data["userId"], - "length": len(evt_ranking), - "userEventRankingList": evt_ranking, + "length": len(prep_event_ranking), + "userEventRankingList": prep_event_ranking, } def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: @@ -788,6 +817,7 @@ class OngekiBase: tmp.pop("user") mission_point_list.append(tmp) + return { "userId": data["userId"], "length": len(mission_point_list), @@ -804,6 +834,10 @@ class OngekiBase: tmp = evt_music._asdict() tmp.pop("id") tmp.pop("user") + # pop other stuff event_point doesn't want + tmp.pop("rank") + tmp.pop("type") + tmp.pop("date") event_point_list.append(tmp) return { @@ -987,6 +1021,9 @@ class OngekiBase: for x in upsert["userTechEventList"]: self.data.item.put_tech_event(user_id, x) + # This should be updated once a day in maintenance window, but for time being we will push the update on each upsert + self.data.item.put_tech_event_ranking(user_id, x) + if "userKopList" in upsert: for x in upsert["userKopList"]: self.data.profile.put_kop(user_id, x) diff --git a/titles/ongeki/const.py b/titles/ongeki/const.py index 8d4c4ab..5c72403 100644 --- a/titles/ongeki/const.py +++ b/titles/ongeki/const.py @@ -19,7 +19,6 @@ class OngekiConstants: EVT_TYPES: Enum = Enum( "EVT_TYPES", [ - "None", "Announcement", "Movie", "AddMyList", @@ -39,6 +38,8 @@ class OngekiConstants: "TechChallengeEvent", "AcceptTechChallengeEvent", "SilverJewelEvent", + "Max", + "None", ], ) diff --git a/titles/ongeki/schema/item.py b/titles/ongeki/schema/item.py index dcaf286..4d0dcfd 100644 --- a/titles/ongeki/schema/item.py +++ b/titles/ongeki/schema/item.py @@ -1,3 +1,4 @@ +from datetime import date, datetime, timedelta from typing import Dict, Optional, List from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON @@ -172,6 +173,9 @@ event_point = Table( Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), Column("eventId", Integer), Column("point", Integer), + Column("rank", Integer), + Column("type", Integer), + Column("date", String(25)), Column("isRankingRewarded", Boolean), UniqueConstraint("user", "eventId", name="ongeki_user_event_point_uk"), mysql_charset="utf8mb4", @@ -247,6 +251,31 @@ tech_event = Table( mysql_charset="utf8mb4", ) +tech_music = Table( + "ongeki_tech_music_list", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("eventId", Integer), + Column("musicId", Integer), + Column("level", Integer), + UniqueConstraint("musicId", name="ongeki_tech_music_list_uk"), + mysql_charset="utf8mb4", +) + +tech_ranking = Table( + "ongeki_tech_event_ranking", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("date", String(25)), + Column("eventId", Integer), + Column("rank", Integer), + Column("totalPlatinumScore", Integer), + Column("totalTechScore", Integer), + UniqueConstraint("user", "eventId", name="ongeki_tech_event_ranking_uk"), + mysql_charset="utf8mb4", +) + gacha = Table( "ongeki_user_gacha", metadata, @@ -534,7 +563,12 @@ class OngekiItemData(BaseData): return result.fetchall() def put_event_point(self, aime_id: int, event_point_data: Dict) -> Optional[int]: + # We update only the newest (type: 1) entry, in official spec game watches for both latest(type:1) and previous (type:2) entries to give an additional info how many ranks has player moved up or down + # This fully featured is on TODO list, at the moment we just update the tables as data comes and give out rank as request comes event_point_data["user"] = aime_id + event_point_data["type"] = 1 + event_point_time = datetime.now() + event_point_data["date"] = datetime.strftime(event_point_time, "%Y-%m-%d %H:%M") sql = insert(event_point).values(**event_point_data) conflict = sql.on_duplicate_key_update(**event_point_data) @@ -613,6 +647,14 @@ class OngekiItemData(BaseData): return None return result.fetchall() + def get_tech_music(self) -> Optional[List[Dict]]: + sql = select(tech_music) + result = self.execute(sql) + + if result is None: + return None + return result.fetchall() + def put_tech_event(self, aime_id: int, tech_event_data: Dict) -> Optional[int]: tech_event_data["user"] = aime_id @@ -625,6 +667,22 @@ class OngekiItemData(BaseData): return None return result.lastrowid + def put_tech_event_ranking(self, aime_id: int, tech_event_data: Dict) -> Optional[int]: + tech_event_data["user"] = aime_id + tech_event_data.pop("isRankingRewarded") + tech_event_data.pop("isTotalTechNewRecord") + tech_event_data["date"] = tech_event_data.pop("techRecordDate") + tech_event_data["rank"] = 0 + + sql = insert(tech_ranking).values(**tech_event_data) + conflict = sql.on_duplicate_key_update(**tech_event_data) + result = self.execute(conflict) + + if result is None: + self.logger.warning(f"put_tech_event_ranking: Failed to update ranking! aime_id {aime_id}") + return None + return result.lastrowid + def get_tech_event(self, aime_id: int) -> Optional[List[Dict]]: sql = select(tech_event).where(tech_event.c.user == aime_id) result = self.execute(sql) @@ -714,3 +772,21 @@ class OngekiItemData(BaseData): ) return None return result.lastrowid + + + def get_ranking_event_ranks(self, aime_id: int) -> Optional[List[Dict]]: + # Calculates player rank on GameRequest from server, and sends it back, official spec would rank players in maintenance period, on TODO list + sql = select(event_point.c.id, event_point.c.user, event_point.c.eventId, event_point.c.type, func.row_number().over(partition_by=event_point.c.eventId, order_by=event_point.c.point.desc()).label('rank'), event_point.c.date, event_point.c.point) + result = self.execute(sql) + if result is None: + self.logger.error(f"failed to rank aime_id: {aime_id} ranking event positions") + return None + return result.fetchall() + + def get_tech_event_ranking(self, aime_id: int) -> Optional[List[Dict]]: + sql = select(tech_ranking.c.id, tech_ranking.c.user, tech_ranking.c.date, tech_ranking.c.eventId, func.row_number().over(partition_by=tech_ranking.c.eventId, order_by=[tech_ranking.c.totalTechScore.desc(),tech_ranking.c.totalPlatinumScore.desc()]).label('rank'), tech_ranking.c.totalTechScore, tech_ranking.c.totalPlatinumScore) + result = self.execute(sql) + if result is None: + self.logger.warning(f"aime_id: {aime_id} has no tech ranking ranks") + return None + return result.fetchall() From 1897e8002dfc643423efdafb8166e28a47fae42b Mon Sep 17 00:00:00 2001 From: phantomlan Date: Tue, 7 Nov 2023 02:14:15 +0100 Subject: [PATCH 059/101] add RewardList support add PresentList support add reading rewards to read.py add Ranking Music List support --- titles/ongeki/base.py | 50 +++++++++++++++++++-- titles/ongeki/const.py | 28 ++++++++++++ titles/ongeki/read.py | 26 +++++++++++ titles/ongeki/schema/static.py | 81 ++++++++++++++++++++++++++++++++++ 4 files changed, 181 insertions(+), 4 deletions(-) diff --git a/titles/ongeki/base.py b/titles/ongeki/base.py index ab7c3ec..03ae18a 100644 --- a/titles/ongeki/base.py +++ b/titles/ongeki/base.py @@ -157,7 +157,21 @@ class OngekiBase: return {"type": data["type"], "length": 0, "gameIdlistList": []} def handle_get_game_ranking_api_request(self, data: Dict) -> Dict: - return {"length": 0, "gameRankingList": []} + game_ranking_list = self.data.static.get_ranking_list() + + ranking_list = [] + for music in game_ranking_list: + tmp = music._asdict() + ranking_list.append(tmp) + + if ranking_list is None: + return {"length": 0, "gameRankingList": []} + return { + "type": data["type"], + #"length": len(ranking_list), + "gameRankingList": ranking_list, + } + def handle_get_game_point_api_request(self, data: Dict) -> Dict: """ @@ -215,11 +229,39 @@ class OngekiBase: return {"returnCode": 1, "apiName": "ExtendLockTimeApi"} def handle_get_game_reward_api_request(self, data: Dict) -> Dict: - # TODO: reward list - return {"length": 0, "gameRewardList": []} + get_game_rewards = self.data.static.get_reward_list(self.version) + + reward_list = [] + for reward in get_game_rewards: + tmp = reward._asdict() + tmp.pop("id") + tmp.pop("version") + tmp.pop("rewardname") + reward_list.append(tmp) + + if reward_list is None: + return {"length": 0, "gameRewardList": []} + return { + "length": len(reward_list), + "gameRewardList": reward_list, + } def handle_get_game_present_api_request(self, data: Dict) -> Dict: - return {"length": 0, "gamePresentList": []} + get_present = self.data.static.get_present_list(self.version) + + present_list = [] + for present in get_present: + tmp = present._asdict() + tmp.pop("id") + tmp.pop("version") + present_list.append(tmp) + + if present_list is None: + return {"length": 0, "gamePresentList": []} + return { + "length": len(present_list), + "gamePresentList": present_list, + } def handle_get_game_message_api_request(self, data: Dict) -> Dict: return {"length": 0, "gameMessageList": []} diff --git a/titles/ongeki/const.py b/titles/ongeki/const.py index 5c72403..a7658f7 100644 --- a/titles/ongeki/const.py +++ b/titles/ongeki/const.py @@ -43,6 +43,34 @@ class OngekiConstants: ], ) + REWARD_TYPES: Enum = Enum( + "REWARD_TYPES", + [ + "Card", + "NamePlate", + "Trophy", + "LimitBreakItem", + "AlmightyJewel", + "Money", + "Music", + "ProfileVoice", + "Present", + "ChapterJewel", + "GachaTicket", + "KaikaItem", + "ExpUpItem", + "IntimateUpItem", + "BookItem", + "SystemVoice", + "Costume", + "Medal", + "Attachment", + "UnlockItem", + "Max", + "None", + ], + ) + class CM_GACHA_KINDS(Enum): Normal = 0 Pickup = 1 diff --git a/titles/ongeki/read.py b/titles/ongeki/read.py index b64194f..6e94094 100644 --- a/titles/ongeki/read.py +++ b/titles/ongeki/read.py @@ -44,6 +44,7 @@ class OngekiReader(BaseReader): self.read_events(f"{dir}/event") self.read_music(f"{dir}/music") self.read_card(f"{dir}/card") + self.read_reward(f"{dir}/reward") def read_card(self, base_dir: str) -> None: self.logger.info(f"Reading cards from {base_dir}...") @@ -171,3 +172,28 @@ class OngekiReader(BaseReader): self.version, song_id, chart_id, title, artist, genre, level ) self.logger.info(f"Added song {song_id} chart {chart_id}") + + def read_reward(self, base_dir: str) -> None: + self.logger.info(f"Reading rewards from {base_dir}...") + + for root, dirs, files in os.walk(base_dir): + for dir in dirs: + if os.path.exists(f"{root}/{dir}/Reward.xml"): + strdata = "" + + with open(f"{root}/{dir}/Reward.xml", "r", encoding="utf-8") as f: + strdata = f.read() + + troot = ET.fromstring(strdata) + + if root is None: + continue + + name = troot.find("Name") + rewardId = name.find("id").text + rewardname = name.find("str").text + itemKind = OngekiConstants.REWARD_TYPES[troot.find("ItemType").text].value + itemId = troot.find("RewardItem").find("ItemName").find("id").text + + self.data.static.put_reward(self.version, rewardId, rewardname, itemKind, itemId) + self.logger.info(f"Added reward {rewardId}") diff --git a/titles/ongeki/schema/static.py b/titles/ongeki/schema/static.py index 829b6be..730eb4c 100644 --- a/titles/ongeki/schema/static.py +++ b/titles/ongeki/schema/static.py @@ -96,6 +96,45 @@ cards = Table( mysql_charset="utf8mb4", ) +music_ranking = Table( + "ongeki_static_music_ranking_list", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("musicId", Integer, nullable=False), + Column("point", Integer, nullable=False), + Column("userName", String(255)), + UniqueConstraint("musicId", name="ongeki_static_music_ranking_uk"), + mysql_charset="utf8mb4", +) + +rewards = Table( + "ongeki_static_rewards", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer, nullable=False), + Column("rewardId", Integer, nullable=False), + Column("rewardname", String(255), nullable=False), + Column("itemKind", Integer, nullable=False), + Column("itemId", Integer, nullable=False), + UniqueConstraint("version","itemKind","rewardId", name="ongeki_static_rewards_uk"), + mysql_charset="utf8mb4", +) + +present = Table( + "ongeki_static_present_list", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer, nullable=False), + Column("presentId", Integer, nullable=False), + Column("presentName", String(255), nullable=False), + Column("rewardId", Integer, nullable=False), + Column("stock", Integer, nullable=False), + Column("message", String(255)), + Column("startDate", String(25), nullable=False), + Column("endDate", String(25), nullable=False), + UniqueConstraint("version","presentId", name="ongeki_static_present_list_uk"), + mysql_charset="utf8mb4", +) class OngekiStaticData(BaseData): def put_card(self, version: int, card_id: int, **card_data) -> Optional[int]: @@ -333,3 +372,45 @@ class OngekiStaticData(BaseData): if result is None: return None return result.fetchone() + + def get_ranking_list(self) -> Optional[List[Dict]]: + sql = select(music_ranking.c.musicId.label('id'), music_ranking.c.point, music_ranking.c.userName) + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def put_reward(self, version: int, rewardId: int, rewardname: str, itemKind: int, itemId: int) -> Optional[int]: + sql = insert(rewards).values( + version=version, + rewardId=rewardId, + rewardname=rewardname, + itemKind=itemKind, + itemId=itemId, + ) + conflict = sql.on_duplicate_key_update( + rewardname=rewardname, + ) + result = self.execute(conflict) + if result is None: + self.logger.warning(f"Failed to insert reward! reward_id: {rewardId}") + return None + return result.lastrowid + + def get_reward_list(self, version: int) -> Optional[List[Dict]]: + sql = select(rewards).where(rewards.c.version == version) + + result = self.execute(sql) + if result is None: + self.logger.warning(f"Failed to load reward list") + return None + return result.fetchall() + + def get_present_list(self, version: int) -> Optional[List[Dict]]: + sql = select(present).where(present.c.version == version) + + result = self.execute(sql) + if result is None: + self.logger.warning(f"Failed to load present list") + return None + return result.fetchall() From d2e2c14074299ae2a8da1fd66f97d26e57f03d27 Mon Sep 17 00:00:00 2001 From: phantomlan Date: Tue, 7 Nov 2023 04:38:04 +0100 Subject: [PATCH 060/101] update Event Ranking, Tech Challenge, and Music Ranking --- titles/ongeki/base.py | 19 +++++------ titles/ongeki/schema/item.py | 58 +++++++++++++--------------------- titles/ongeki/schema/static.py | 26 +++++++++++++-- 3 files changed, 56 insertions(+), 47 deletions(-) diff --git a/titles/ongeki/base.py b/titles/ongeki/base.py index 03ae18a..ad64c15 100644 --- a/titles/ongeki/base.py +++ b/titles/ongeki/base.py @@ -157,7 +157,7 @@ class OngekiBase: return {"type": data["type"], "length": 0, "gameIdlistList": []} def handle_get_game_ranking_api_request(self, data: Dict) -> Dict: - game_ranking_list = self.data.static.get_ranking_list() + game_ranking_list = self.data.static.get_ranking_list(self.version) ranking_list = [] for music in game_ranking_list: @@ -168,7 +168,6 @@ class OngekiBase: return {"length": 0, "gameRankingList": []} return { "type": data["type"], - #"length": len(ranking_list), "gameRankingList": ranking_list, } @@ -270,12 +269,13 @@ class OngekiBase: return {"length": 0, "gameSaleList": []} def handle_get_game_tech_music_api_request(self, data: Dict) -> Dict: - music_list = self.data.item.get_tech_music() + music_list = self.data.static.get_tech_music(self.version) prep_music_list = [] for music in music_list: tmp = music._asdict() tmp.pop("id") + tmp.pop("version") prep_music_list.append(tmp) if prep_music_list is None: @@ -441,7 +441,7 @@ class OngekiBase: } def handle_get_user_tech_event_api_request(self, data: Dict) -> Dict: - user_tech_event_list = self.data.item.get_tech_event(data["userId"]) + user_tech_event_list = self.data.item.get_tech_event(self.version, data["userId"]) if user_tech_event_list is None: return {} @@ -450,6 +450,7 @@ class OngekiBase: tmp = evt._asdict() tmp.pop("id") tmp.pop("user") + tmp.pop("version") tech_evt.append(tmp) return { @@ -459,7 +460,7 @@ class OngekiBase: } def handle_get_user_tech_event_ranking_api_request(self, data: Dict) -> Dict: - user_tech_event_ranks = self.data.item.get_tech_event_ranking(data["userId"]) + user_tech_event_ranks = self.data.item.get_tech_event_ranking(self.version, data["userId"]) if user_tech_event_ranks is None: return { "userId": data["userId"], @@ -598,7 +599,7 @@ class OngekiBase: return {"userId": data["userId"], "userData": user_data} def handle_get_user_event_ranking_api_request(self, data: Dict) -> Dict: - user_event_ranking_list = self.data.item.get_ranking_event_ranks(data["userId"]) + user_event_ranking_list = self.data.item.get_ranking_event_ranks(self.version, data["userId"]) if user_event_ranking_list is None: return {} @@ -1027,7 +1028,7 @@ class OngekiBase: if "userEventPointList" in upsert: for x in upsert["userEventPointList"]: - self.data.item.put_event_point(user_id, x) + self.data.item.put_event_point(user_id, self.version, x) if "userMissionPointList" in upsert: for x in upsert["userMissionPointList"]: @@ -1061,10 +1062,10 @@ class OngekiBase: if "userTechEventList" in upsert: for x in upsert["userTechEventList"]: - self.data.item.put_tech_event(user_id, x) + self.data.item.put_tech_event(user_id, self.version, x) # This should be updated once a day in maintenance window, but for time being we will push the update on each upsert - self.data.item.put_tech_event_ranking(user_id, x) + self.data.item.put_tech_event_ranking(user_id, self.version, x) if "userKopList" in upsert: for x in upsert["userKopList"]: diff --git a/titles/ongeki/schema/item.py b/titles/ongeki/schema/item.py index 4d0dcfd..0e82f16 100644 --- a/titles/ongeki/schema/item.py +++ b/titles/ongeki/schema/item.py @@ -171,10 +171,11 @@ event_point = Table( metadata, Column("id", Integer, primary_key=True, nullable=False), Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), - Column("eventId", Integer), - Column("point", Integer), + Column("version", Integer, nullable=False), + Column("eventId", Integer, nullable=False), + Column("point", Integer, nullable=False), Column("rank", Integer), - Column("type", Integer), + Column("type", Integer, nullable=False), Column("date", String(25)), Column("isRankingRewarded", Boolean), UniqueConstraint("user", "eventId", name="ongeki_user_event_point_uk"), @@ -241,9 +242,10 @@ tech_event = Table( metadata, Column("id", Integer, primary_key=True, nullable=False), Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), - Column("eventId", Integer), - Column("totalTechScore", Integer), - Column("totalPlatinumScore", Integer), + Column("version", Integer, nullable=False), + Column("eventId", Integer, nullable=False), + Column("totalTechScore", Integer, nullable=False), + Column("totalPlatinumScore", Integer, nullable=False), Column("techRecordDate", String(25)), Column("isRankingRewarded", Boolean), Column("isTotalTechNewRecord", Boolean), @@ -251,27 +253,17 @@ tech_event = Table( mysql_charset="utf8mb4", ) -tech_music = Table( - "ongeki_tech_music_list", - metadata, - Column("id", Integer, primary_key=True, nullable=False), - Column("eventId", Integer), - Column("musicId", Integer), - Column("level", Integer), - UniqueConstraint("musicId", name="ongeki_tech_music_list_uk"), - mysql_charset="utf8mb4", -) - tech_ranking = Table( "ongeki_tech_event_ranking", metadata, Column("id", Integer, primary_key=True, nullable=False), Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("version", Integer, nullable=False), Column("date", String(25)), - Column("eventId", Integer), + Column("eventId", Integer, nullable=False), Column("rank", Integer), - Column("totalPlatinumScore", Integer), - Column("totalTechScore", Integer), + Column("totalPlatinumScore", Integer, nullable=False), + Column("totalTechScore", Integer, nullable=False), UniqueConstraint("user", "eventId", name="ongeki_tech_event_ranking_uk"), mysql_charset="utf8mb4", ) @@ -562,10 +554,11 @@ class OngekiItemData(BaseData): return None return result.fetchall() - def put_event_point(self, aime_id: int, event_point_data: Dict) -> Optional[int]: + def put_event_point(self, aime_id: int, version: int, event_point_data: Dict) -> Optional[int]: # We update only the newest (type: 1) entry, in official spec game watches for both latest(type:1) and previous (type:2) entries to give an additional info how many ranks has player moved up or down # This fully featured is on TODO list, at the moment we just update the tables as data comes and give out rank as request comes event_point_data["user"] = aime_id + event_point_data["version"] = version event_point_data["type"] = 1 event_point_time = datetime.now() event_point_data["date"] = datetime.strftime(event_point_time, "%Y-%m-%d %H:%M") @@ -647,14 +640,6 @@ class OngekiItemData(BaseData): return None return result.fetchall() - def get_tech_music(self) -> Optional[List[Dict]]: - sql = select(tech_music) - result = self.execute(sql) - - if result is None: - return None - return result.fetchall() - def put_tech_event(self, aime_id: int, tech_event_data: Dict) -> Optional[int]: tech_event_data["user"] = aime_id @@ -667,8 +652,9 @@ class OngekiItemData(BaseData): return None return result.lastrowid - def put_tech_event_ranking(self, aime_id: int, tech_event_data: Dict) -> Optional[int]: + def put_tech_event_ranking(self, version: int, aime_id: int, tech_event_data: Dict) -> Optional[int]: tech_event_data["user"] = aime_id + tech_event_data["version"] = version tech_event_data.pop("isRankingRewarded") tech_event_data.pop("isTotalTechNewRecord") tech_event_data["date"] = tech_event_data.pop("techRecordDate") @@ -683,8 +669,8 @@ class OngekiItemData(BaseData): return None return result.lastrowid - def get_tech_event(self, aime_id: int) -> Optional[List[Dict]]: - sql = select(tech_event).where(tech_event.c.user == aime_id) + def get_tech_event(self, version: int, aime_id: int) -> Optional[List[Dict]]: + sql = select(tech_event).where(and_(tech_event.c.user == aime_id, tech_event.c.version == version)) result = self.execute(sql) if result is None: @@ -774,17 +760,17 @@ class OngekiItemData(BaseData): return result.lastrowid - def get_ranking_event_ranks(self, aime_id: int) -> Optional[List[Dict]]: + def get_ranking_event_ranks(self, version: int, aime_id: int) -> Optional[List[Dict]]: # Calculates player rank on GameRequest from server, and sends it back, official spec would rank players in maintenance period, on TODO list - sql = select(event_point.c.id, event_point.c.user, event_point.c.eventId, event_point.c.type, func.row_number().over(partition_by=event_point.c.eventId, order_by=event_point.c.point.desc()).label('rank'), event_point.c.date, event_point.c.point) + sql = select(event_point.c.id, event_point.c.user, event_point.c.eventId, event_point.c.type, func.row_number().over(partition_by=event_point.c.eventId, order_by=event_point.c.point.desc()).label('rank'), event_point.c.date, event_point.c.point).where(event_point.c.version == version) result = self.execute(sql) if result is None: self.logger.error(f"failed to rank aime_id: {aime_id} ranking event positions") return None return result.fetchall() - def get_tech_event_ranking(self, aime_id: int) -> Optional[List[Dict]]: - sql = select(tech_ranking.c.id, tech_ranking.c.user, tech_ranking.c.date, tech_ranking.c.eventId, func.row_number().over(partition_by=tech_ranking.c.eventId, order_by=[tech_ranking.c.totalTechScore.desc(),tech_ranking.c.totalPlatinumScore.desc()]).label('rank'), tech_ranking.c.totalTechScore, tech_ranking.c.totalPlatinumScore) + def get_tech_event_ranking(self, version: int, aime_id: int) -> Optional[List[Dict]]: + sql = select(tech_ranking.c.id, tech_ranking.c.user, tech_ranking.c.date, tech_ranking.c.eventId, func.row_number().over(partition_by=tech_ranking.c.eventId, order_by=[tech_ranking.c.totalTechScore.desc(),tech_ranking.c.totalPlatinumScore.desc()]).label('rank'), tech_ranking.c.totalTechScore, tech_ranking.c.totalPlatinumScore).where(tech_ranking.c.version == version) result = self.execute(sql) if result is None: self.logger.warning(f"aime_id: {aime_id} has no tech ranking ranks") diff --git a/titles/ongeki/schema/static.py b/titles/ongeki/schema/static.py index 730eb4c..16aedbd 100644 --- a/titles/ongeki/schema/static.py +++ b/titles/ongeki/schema/static.py @@ -100,6 +100,7 @@ music_ranking = Table( "ongeki_static_music_ranking_list", metadata, Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer, nullable=False), Column("musicId", Integer, nullable=False), Column("point", Integer, nullable=False), Column("userName", String(255)), @@ -136,6 +137,18 @@ present = Table( mysql_charset="utf8mb4", ) +tech_music = Table( + "ongeki_static_tech_music", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer, nullable=False), + Column("eventId", Integer, nullable=False), + Column("musicId", Integer, nullable=False), + Column("level", Integer, nullable=False), + UniqueConstraint("version", "musicId", name="ongeki_static_tech_music_uk"), + mysql_charset="utf8mb4", +) + class OngekiStaticData(BaseData): def put_card(self, version: int, card_id: int, **card_data) -> Optional[int]: sql = insert(cards).values(version=version, cardId=card_id, **card_data) @@ -373,8 +386,8 @@ class OngekiStaticData(BaseData): return None return result.fetchone() - def get_ranking_list(self) -> Optional[List[Dict]]: - sql = select(music_ranking.c.musicId.label('id'), music_ranking.c.point, music_ranking.c.userName) + def get_ranking_list(self, version: int) -> Optional[List[Dict]]: + sql = select(music_ranking.c.musicId.label('id'), music_ranking.c.point, music_ranking.c.userName).where(music_ranking.c.version == version) result = self.execute(sql) if result is None: return None @@ -414,3 +427,12 @@ class OngekiStaticData(BaseData): self.logger.warning(f"Failed to load present list") return None return result.fetchall() + + def get_tech_music(self, version: int) -> Optional[List[Dict]]: + sql = select(tech_music).where(tech_music.c.version == version) + + result = self.execute(sql) + + if result is None: + return None + return result.fetchall() From f81c53558ec63d154a907be49c98956e64007d03 Mon Sep 17 00:00:00 2001 From: phantomlan Date: Thu, 9 Nov 2023 02:40:57 +0100 Subject: [PATCH 061/101] - Add ClientTestmode upsert - Add ClientSetting upsert - Add endDate for Events - Add Upgrade Schema - Small bugfixes for events --- core/data/schema/versions/SDDT_6_upgrade.sql | 88 ++++++++++++++++++++ docs/game_specific_info.md | 7 +- titles/ongeki/base.py | 24 ++++-- titles/ongeki/schema/item.py | 11 ++- titles/ongeki/schema/static.py | 52 +++++++++++- 5 files changed, 166 insertions(+), 16 deletions(-) create mode 100644 core/data/schema/versions/SDDT_6_upgrade.sql diff --git a/core/data/schema/versions/SDDT_6_upgrade.sql b/core/data/schema/versions/SDDT_6_upgrade.sql new file mode 100644 index 0000000..3bc6a8e --- /dev/null +++ b/core/data/schema/versions/SDDT_6_upgrade.sql @@ -0,0 +1,88 @@ +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) +); +SET FOREIGN_KEY_CHECKS=1; diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index c338279..4ea32a0 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -343,7 +343,9 @@ Events are controlled by 2 types of enabled events: - 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 @@ -352,11 +354,14 @@ AcceptRankingEvent and AcceptTechChallengeEvent are reward period for events, wh 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_tech_music_list` +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) diff --git a/titles/ongeki/base.py b/titles/ongeki/base.py index ad64c15..7e67fd0 100644 --- a/titles/ongeki/base.py +++ b/titles/ongeki/base.py @@ -287,9 +287,21 @@ class OngekiBase: } def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict: + if self.core_cfg.server.is_develop: + return {"returnCode": 1, "apiName": "UpsertClientSettingApi"} + + client_id = data["clientId"] + client_setting_data = data["clientSetting"] + self.data.static.put_client_setting_data(client_id, client_setting_data) return {"returnCode": 1, "apiName": "UpsertClientSettingApi"} def handle_upsert_client_testmode_api_request(self, data: Dict) -> Dict: + if self.core_cfg.server.is_develop: + return {"returnCode": 1, "apiName": "UpsertClientTestmodeApi"} + + region_id = data["regionId"] + client_testmode_data = data["clientTestmode"] + self.data.static.put_client_testmode_data(region_id, client_testmode_data) return {"returnCode": 1, "apiName": "UpsertClientTestmodeApi"} def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict: @@ -333,10 +345,9 @@ class OngekiBase: "id": event["eventId"], # actually use the startDate from the import so it # properly shows all the events when new ones are imported - "startDate": datetime.strftime( - event["startDate"], "%Y-%m-%d %H:%M:%S.0" - ), - "endDate": "2099-12-31 00:00:00.0", + "startDate": datetime.strftime(event["startDate"], "%Y-%m-%d %H:%M:%S.0"), + #"endDate": "2099-12-31 00:00:00.0", + "endDate": datetime.strftime(event["endDate"], "%Y-%m-%d %H:%M:%S.0"), } ) @@ -849,7 +860,7 @@ class OngekiBase: } def handle_get_user_mission_point_api_request(self, data: Dict) -> Dict: - user_mission_point_list = self.data.item.get_mission_points(data["userId"]) + user_mission_point_list = self.data.item.get_mission_points(self.version, data["userId"]) if user_mission_point_list is None: return {} @@ -858,6 +869,7 @@ class OngekiBase: tmp = evt_music._asdict() tmp.pop("id") tmp.pop("user") + tmp.pop("version") mission_point_list.append(tmp) @@ -1032,7 +1044,7 @@ class OngekiBase: if "userMissionPointList" in upsert: for x in upsert["userMissionPointList"]: - self.data.item.put_mission_point(user_id, x) + self.data.item.put_mission_point(user_id, self.version, x) if "userRatinglogList" in upsert: for x in upsert["userRatinglogList"]: diff --git a/titles/ongeki/schema/item.py b/titles/ongeki/schema/item.py index 0e82f16..a7854c8 100644 --- a/titles/ongeki/schema/item.py +++ b/titles/ongeki/schema/item.py @@ -187,6 +187,7 @@ mission_point = Table( metadata, Column("id", Integer, primary_key=True, nullable=False), Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("version", Integer), Column("eventId", Integer), Column("point", Integer), UniqueConstraint("user", "eventId", name="ongeki_user_mission_point_uk"), @@ -337,7 +338,6 @@ print_detail = Table( mysql_charset="utf8mb4", ) - class OngekiItemData(BaseData): def put_card(self, aime_id: int, card_data: Dict) -> Optional[int]: card_data["user"] = aime_id @@ -532,10 +532,9 @@ class OngekiItemData(BaseData): return None return result.fetchall() - def put_mission_point( - self, aime_id: int, mission_point_data: Dict - ) -> Optional[int]: + def put_mission_point(self, version: int, aime_id: int, mission_point_data: Dict) -> Optional[int]: mission_point_data["user"] = aime_id + mission_point_data["version"] = version sql = insert(mission_point).values(**mission_point_data) conflict = sql.on_duplicate_key_update(**mission_point_data) @@ -546,8 +545,8 @@ class OngekiItemData(BaseData): return None return result.lastrowid - def get_mission_points(self, aime_id: int) -> Optional[List[Dict]]: - sql = select(mission_point).where(mission_point.c.user == aime_id) + def get_mission_points(self, version: int, aime_id: int) -> Optional[List[Dict]]: + sql = select(mission_point).where(and_(mission_point.c.user == aime_id, mission_point.c.version == version)) result = self.execute(sql) if result is None: diff --git a/titles/ongeki/schema/static.py b/titles/ongeki/schema/static.py index 16aedbd..fe9d468 100644 --- a/titles/ongeki/schema/static.py +++ b/titles/ongeki/schema/static.py @@ -17,6 +17,7 @@ events = Table( Column("type", Integer), Column("name", String(255)), Column("startDate", TIMESTAMP, server_default=func.now()), + Column("endDate", TIMESTAMP, server_default=func.now()), Column("enabled", Boolean, server_default="1"), UniqueConstraint("version", "eventId", "type", name="ongeki_static_events_uk"), mysql_charset="utf8mb4", @@ -104,7 +105,7 @@ music_ranking = Table( Column("musicId", Integer, nullable=False), Column("point", Integer, nullable=False), Column("userName", String(255)), - UniqueConstraint("musicId", name="ongeki_static_music_ranking_uk"), + UniqueConstraint("version", "musicId", name="ongeki_static_music_ranking_uk"), mysql_charset="utf8mb4", ) @@ -117,7 +118,7 @@ rewards = Table( Column("rewardname", String(255), nullable=False), Column("itemKind", Integer, nullable=False), Column("itemId", Integer, nullable=False), - UniqueConstraint("version","itemKind","rewardId", name="ongeki_static_rewards_uk"), + UniqueConstraint("version", "rewardId", name="ongeki_static_rewards_uk"), mysql_charset="utf8mb4", ) @@ -133,7 +134,7 @@ present = Table( Column("message", String(255)), Column("startDate", String(25), nullable=False), Column("endDate", String(25), nullable=False), - UniqueConstraint("version","presentId", name="ongeki_static_present_list_uk"), + UniqueConstraint("version", "presentId", "rewardId", name="ongeki_static_present_list_uk", mysql_charset="utf8mb4", ) @@ -149,6 +150,30 @@ tech_music = Table( mysql_charset="utf8mb4", ) +client_testmode = Table( + "ongeki_static_client_testmode", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("regionId", Integer, nullable=False), + Column("placeId", Integer, nullable=False), + Column("clientId", String(11), nullable=False), + Column("updateDate", TIMESTAMP, nullable=False), + Column("isDelivery", Boolean, nullable=False), + Column("groupId", Integer, nullable=False), + Column("groupRole", Integer, nullable=False), + Column("continueMode", Integer, nullable=False), + Column("selectMusicTime", Integer, nullable=False), + Column("advertiseVolume", Integer, nullable=False), + Column("eventMode", Integer, nullable=False), + Column("eventMusicNum", Integer, nullable=False), + Column("patternGp", Integer, nullable=False), + Column("limitGp", Integer, nullable=False), + Column("maxLeverMovable", Integer, nullable=False), + Column("minLeverMovable", Integer, nullable=False), + UniqueConstraint("clientId", name="ongeki_static_client_testmode_uk"), + mysql_charset="utf8mb4", +) + class OngekiStaticData(BaseData): def put_card(self, version: int, card_id: int, **card_data) -> Optional[int]: sql = insert(cards).values(version=version, cardId=card_id, **card_data) @@ -287,6 +312,7 @@ class OngekiStaticData(BaseData): eventId=event_id, type=event_type, name=event_name, + endDate=f"2038-01-01 00:00:00", ) conflict = sql.on_duplicate_key_update( @@ -436,3 +462,23 @@ class OngekiStaticData(BaseData): if result is None: return None return result.fetchall() + + def put_client_testmode_data(self, region_id: int, client_testmode_data: Dict) -> Optional[List[Dict]]: + sql = insert(client_testmode).values(regionId=region_id, **client_testmode_data) + conflict = sql.on_duplicate_key_update(regionId=region_id, **client_testmode_data) + + result = self.execute(conflict) + if result is None: + self.logger.warning(f"clientId: {clientId} Failed to update ClientTestMode data"), + return None + return result.lastrowid + + def put_client_setting_data(self, client_id: str, client_setting_data: Dict) -> Optional[List[Dict]]: + sql = insert(machine).values(data=client_setting_data) + conflict = sql.on_duplicate_key_update(serial=client_id) + + result = self.execute(conflict) + if result is None: + self.logger.warning(f"clientId: {clientId} Failed to update ClientSetting data"), + return None + return result.lastrowid From 530016ef84ff1335fe480415d76d4e22275cec37 Mon Sep 17 00:00:00 2001 From: phantomlan Date: Thu, 9 Nov 2023 03:10:48 +0100 Subject: [PATCH 062/101] i ate one ) --- titles/ongeki/schema/static.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/ongeki/schema/static.py b/titles/ongeki/schema/static.py index fe9d468..5b459b8 100644 --- a/titles/ongeki/schema/static.py +++ b/titles/ongeki/schema/static.py @@ -134,7 +134,7 @@ present = Table( Column("message", String(255)), Column("startDate", String(25), nullable=False), Column("endDate", String(25), nullable=False), - UniqueConstraint("version", "presentId", "rewardId", name="ongeki_static_present_list_uk", + UniqueConstraint("version", "presentId", "rewardId", name="ongeki_static_present_list_uk"), mysql_charset="utf8mb4", ) From 31d39ffb37e0f1e123c14fceaec5365470d4cdf5 Mon Sep 17 00:00:00 2001 From: phantomlan Date: Thu, 9 Nov 2023 10:29:42 +0100 Subject: [PATCH 063/101] merge conflict fix --- titles/ongeki/index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/ongeki/index.py b/titles/ongeki/index.py index 9cc8891..edb06ba 100644 --- a/titles/ongeki/index.py +++ b/titles/ongeki/index.py @@ -263,4 +263,4 @@ class OngekiServlet(BaseServlet): bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]), ) - return crypt.encrypt(padded) \ No newline at end of file + return crypt.encrypt(padded) From d663b1ef7c009afdcd846c0dd38fa811c9bce716 Mon Sep 17 00:00:00 2001 From: phantomlan Date: Mon, 13 Nov 2023 03:03:04 +0100 Subject: [PATCH 064/101] fix foreign key issue --- titles/ongeki/schema/item.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/ongeki/schema/item.py b/titles/ongeki/schema/item.py index a7854c8..2c5b526 100644 --- a/titles/ongeki/schema/item.py +++ b/titles/ongeki/schema/item.py @@ -532,7 +532,7 @@ class OngekiItemData(BaseData): return None return result.fetchall() - def put_mission_point(self, version: int, aime_id: int, mission_point_data: Dict) -> Optional[int]: + def put_mission_point(self, aime_id: int, version: int, mission_point_data: Dict) -> Optional[int]: mission_point_data["user"] = aime_id mission_point_data["version"] = version From b56a5f020b77129038ba2ba2e64243135ae13399 Mon Sep 17 00:00:00 2001 From: phantomlan Date: Mon, 13 Nov 2023 03:26:09 +0100 Subject: [PATCH 065/101] add rollback sql move GP support to table in DB small fixes --- core/data/schema/versions/SDDT_5_rollback.sql | 22 +++++++ core/data/schema/versions/SDDT_6_upgrade.sql | 10 +++ titles/ongeki/base.py | 65 ++++++------------- titles/ongeki/schema/static.py | 30 ++++++++- 4 files changed, 82 insertions(+), 45 deletions(-) create mode 100644 core/data/schema/versions/SDDT_5_rollback.sql diff --git a/core/data/schema/versions/SDDT_5_rollback.sql b/core/data/schema/versions/SDDT_5_rollback.sql new file mode 100644 index 0000000..08324e0 --- /dev/null +++ b/core/data/schema/versions/SDDT_5_rollback.sql @@ -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_event 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; diff --git a/core/data/schema/versions/SDDT_6_upgrade.sql b/core/data/schema/versions/SDDT_6_upgrade.sql index 3bc6a8e..82d5336 100644 --- a/core/data/schema/versions/SDDT_6_upgrade.sql +++ b/core/data/schema/versions/SDDT_6_upgrade.sql @@ -85,4 +85,14 @@ CREATE TABLE ongeki_static_client_testmode ( 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; diff --git a/titles/ongeki/base.py b/titles/ongeki/base.py index 7e67fd0..2f87b19 100644 --- a/titles/ongeki/base.py +++ b/titles/ongeki/base.py @@ -173,51 +173,28 @@ class OngekiBase: def handle_get_game_point_api_request(self, data: Dict) -> Dict: - """ - Sets the GP amount for A and B sets for 1 - 3 credits - """ - return { - "length": 6, - "gamePointList": [ - { - "type": 0, - "cost": 100, - "startDate": "2000-01-01 05:00:00.0", - "endDate": "2099-01-01 05:00:00.0", - }, - { - "type": 1, - "cost": 230, - "startDate": "2000-01-01 05:00:00.0", - "endDate": "2099-01-01 05:00:00.0", - }, - { - "type": 2, - "cost": 370, - "startDate": "2000-01-01 05:00:00.0", - "endDate": "2099-01-01 05:00:00.0", - }, - { - "type": 3, - "cost": 120, - "startDate": "2000-01-01 05:00:00.0", - "endDate": "2099-01-01 05:00:00.0", - }, - { - "type": 4, - "cost": 240, - "startDate": "2000-01-01 05:00:00.0", - "endDate": "2099-01-01 05:00:00.0", - }, - { - "type": 5, - "cost": 360, - "startDate": "2000-01-01 05:00:00.0", - "endDate": "2099-01-01 05:00:00.0", - }, - ], - } + get_game_point = self.data.static.get_static_game_point() + game_point = [] + if not get_game_point: + self.logger.info(f"GP table is empty, inserting defaults") + self.data.static.put_static_game_point_defaults() + get_game_point = self.data.static.get_static_game_point() + for gp in get_game_point: + tmp = gp._asdict() + game_point.append(tmp) + return { + "length": len(game_point), + "gamePointList": game_point, + } + for gp in get_game_point: + tmp = gp._asdict() + game_point.append(tmp) + return { + "length": len(game_point), + "gamePointList": game_point, + } + def handle_game_login_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "gameLogin"} diff --git a/titles/ongeki/schema/static.py b/titles/ongeki/schema/static.py index 5b459b8..0a5d176 100644 --- a/titles/ongeki/schema/static.py +++ b/titles/ongeki/schema/static.py @@ -134,7 +134,7 @@ present = Table( Column("message", String(255)), Column("startDate", String(25), nullable=False), Column("endDate", String(25), nullable=False), - UniqueConstraint("version", "presentId", "rewardId", name="ongeki_static_present_list_uk"), + UniqueConstraint("version", "presentId", name="ongeki_static_present_list_uk"), mysql_charset="utf8mb4", ) @@ -174,6 +174,18 @@ client_testmode = Table( mysql_charset="utf8mb4", ) +game_point = Table( + "ongeki_static_game_point", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("type", Integer, nullable=False), + Column("cost", Integer, nullable=False), + Column("startDate", String(25), nullable=False, server_default="2000-01-01 05:00:00.0"), + Column("endDate", String(25), nullable=False, server_default="2099-01-01 05:00:00.0"), + UniqueConstraint("type", name="ongeki_static_game_point_uk"), + mysql_charset="utf8mb4", +) + class OngekiStaticData(BaseData): def put_card(self, version: int, card_id: int, **card_data) -> Optional[int]: sql = insert(cards).values(version=version, cardId=card_id, **card_data) @@ -482,3 +494,19 @@ class OngekiStaticData(BaseData): self.logger.warning(f"clientId: {clientId} Failed to update ClientSetting data"), return None return result.lastrowid + + def put_static_game_point_defaults(self) -> Optional[List[Dict]]: + game_point_defaults = [{"type": 0, "cost": 100},{"type": 1, "cost": 230},{"type": 2, "cost": 370},{"type": 3, "cost": 120},{"type": 4, "cost": 240},{"type": 5, "cost": 360}] + sql = insert(game_point).values(game_point_defaults) + result = self.execute(sql) + if result is None: + self.logger.warning(f"Failed to insert default GP table!") + return None + return result.lastrowid + + def get_static_game_point(self) -> Optional[List[Dict]]: + sql = select(game_point.c.type, game_point.c.cost, game_point.c.startDate, game_point.c.endDate) + result = self.execute(sql) + if result is None: + return None + return result.fetchall() From e461ffe5875efeea2e59417219fa88fa38af94cd Mon Sep 17 00:00:00 2001 From: phantomlan Date: Mon, 13 Nov 2023 03:38:30 +0100 Subject: [PATCH 066/101] add more info --- docs/game_specific_info.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index 4ea32a0..2bf6dc4 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -374,6 +374,27 @@ Mission Event (type 13) is a monthly type of event, which is used when another e 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) From 80ae372ccf4cf0eaf7f0692eab2acc9a94347b1b Mon Sep 17 00:00:00 2001 From: phantomlan Date: Mon, 13 Nov 2023 04:05:13 +0100 Subject: [PATCH 067/101] fix rollback --- core/data/schema/versions/SDDT_5_rollback.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/data/schema/versions/SDDT_5_rollback.sql b/core/data/schema/versions/SDDT_5_rollback.sql index 08324e0..007716c 100644 --- a/core/data/schema/versions/SDDT_5_rollback.sql +++ b/core/data/schema/versions/SDDT_5_rollback.sql @@ -9,7 +9,7 @@ ALTER TABLE ongeki_user_tech_event DROP COLUMN version; ALTER TABLE ongeki_user_mission_point DROP COLUMN version; -ALTER TABLE ongeki_static_event DROP COLUMN endDate; +ALTER TABLE ongeki_static_events DROP COLUMN endDate; DROP TABLE ongeki_tech_event_ranking; DROP TABLE ongeki_static_music_ranking_list; From 1a4dc88c8a6925e6da630c304dd9ebbe5ae0ef14 Mon Sep 17 00:00:00 2001 From: Midorica Date: Mon, 13 Nov 2023 10:12:52 -0500 Subject: [PATCH 068/101] cxb: cleanup of config --- example_config/cxb.yaml | 8 +------- titles/cxb/config.py | 36 ------------------------------------ 2 files changed, 1 insertion(+), 43 deletions(-) diff --git a/example_config/cxb.yaml b/example_config/cxb.yaml index 08948bd..7723ff4 100644 --- a/example_config/cxb.yaml +++ b/example_config/cxb.yaml @@ -1,9 +1,3 @@ server: enable: True - loglevel: "info" - hostname: "localhost" - ssl_enable: False - port: 8082 - port_secure: 443 - ssl_cert: "cert/title.crt" - ssl_key: "cert/title.key" + loglevel: "info" \ No newline at end of file diff --git a/titles/cxb/config.py b/titles/cxb/config.py index 00b7290..fa5a6a3 100644 --- a/titles/cxb/config.py +++ b/titles/cxb/config.py @@ -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): def __init__(self) -> None: From 1034a505f08c3a188dd2f3b4e2174ec01b8e8ad8 Mon Sep 17 00:00:00 2001 From: Midorica Date: Mon, 13 Nov 2023 10:43:38 -0500 Subject: [PATCH 069/101] fixing get_allnet_info for cxb, sao and ongeki --- titles/cxb/index.py | 2 +- titles/ongeki/index.py | 8 ++++---- titles/sao/index.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/titles/cxb/index.py b/titles/cxb/index.py index 54e2401..c243c15 100644 --- a/titles/cxb/index.py +++ b/titles/cxb/index.py @@ -77,7 +77,7 @@ class CxbServlet(BaseServlet): return True 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_ssl(self.core_cfg) != 443: + if not self.core_cfg.server.is_using_proxy and Utils.get_title_port_ssl(self.core_cfg): return ( f"https://{self.core_cfg.title.hostname}:{self.core_cfg.title.port_ssl}", "", diff --git a/titles/ongeki/index.py b/titles/ongeki/index.py index edb06ba..c97778b 100644 --- a/titles/ongeki/index.py +++ b/titles/ongeki/index.py @@ -132,10 +132,10 @@ class OngekiServlet(BaseServlet): proto = "https" if self.game_cfg.server.use_https and game_ver >= 120 else "http" if proto == "https": - t_port = f":{title_port_ssl_int}" if title_port_ssl_int != 443 and not self.core_cfg.server.is_using_proxy else "" + t_port = f":{title_port_ssl_int}" if title_port_ssl_int and not self.core_cfg.server.is_using_proxy else "" else: - t_port = f":{title_port_int}" if title_port_int != 80 and not self.core_cfg.server.is_using_proxy else "" + t_port = f":{title_port_int}" if title_port_int and not self.core_cfg.server.is_using_proxy else "" return ( f"{proto}://{self.core_cfg.title.hostname}{t_port}/{game_code}/{game_ver}/", @@ -252,7 +252,7 @@ class OngekiServlet(BaseServlet): zipped = zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) - if not encrtped or version < 120: + if not encrtped and version < 120: return zipped padded = pad(zipped, 16) @@ -263,4 +263,4 @@ class OngekiServlet(BaseServlet): bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]), ) - return crypt.encrypt(padded) + return crypt.encrypt(padded) \ No newline at end of file diff --git a/titles/sao/index.py b/titles/sao/index.py index 63ad3f7..5bbdc7e 100644 --- a/titles/sao/index.py +++ b/titles/sao/index.py @@ -76,7 +76,7 @@ class SaoServlet(BaseServlet): return True 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 self.core_cfg.title.port_ssl: + if not self.core_cfg.server.is_using_proxy and Utils.get_title_port_ssl(self.core_cfg): return ( f"https://{self.core_cfg.title.hostname}:{self.core_cfg.title.port_ssl}/", f"{self.core_cfg.title.hostname}/", From c9a20b243332c2545491e2258227086b585304b8 Mon Sep 17 00:00:00 2001 From: Midorica Date: Mon, 13 Nov 2023 10:47:39 -0500 Subject: [PATCH 070/101] ongeki: fixing render_POST --- titles/ongeki/index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/ongeki/index.py b/titles/ongeki/index.py index c97778b..baf1ee3 100644 --- a/titles/ongeki/index.py +++ b/titles/ongeki/index.py @@ -252,7 +252,7 @@ class OngekiServlet(BaseServlet): zipped = zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) - if not encrtped and version < 120: + if not encrtped or version < 120: return zipped padded = pad(zipped, 16) From e561f8f15d76e58f840813402c25945247a66a45 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Mon, 13 Nov 2023 17:27:14 +0100 Subject: [PATCH 071/101] idac: updated to latest develop branch, minified timeRelease json --- titles/idac/data/timeRelease_v0100.json | Bin 1286433 -> 703770 bytes titles/idac/data/timeRelease_v0131.json | Bin 1863979 -> 1072598 bytes titles/idac/data/timeRelease_v0141.json | Bin 1864737 -> 1072936 bytes titles/idac/data/timeRelease_v0150.json | Bin 1354811 -> 1074358 bytes titles/idac/index.py | 105 ++++++++++++------------ 5 files changed, 52 insertions(+), 53 deletions(-) diff --git a/titles/idac/data/timeRelease_v0100.json b/titles/idac/data/timeRelease_v0100.json index 283693566dbccf0f631adb79e894d266e5c4dc0b..acb5209a0865e049f764e6af09ca17cff2565a5b 100644 GIT binary patch literal 703770 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 literal 1286433 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 DB4?_a diff --git a/titles/idac/data/timeRelease_v0131.json b/titles/idac/data/timeRelease_v0131.json index 184e6d272d6b16475ec524dc42e2f9339d2288cf..4798c30a89f04a4c8035317d003377f7e9ff5e46 100644 GIT binary patch literal 1072598 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 TkNo)j?x&BRmhb<+|M>p^$QS>3 literal 1863979 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 YjBa1w|7rTI-kf}^pC9Fa{qV>C4*`RJ(f|Me diff --git a/titles/idac/data/timeRelease_v0150.json b/titles/idac/data/timeRelease_v0150.json index 694c81957b003e3c2a834dcabe8505174834ce7a..539c138ecb366438140bda29cbf6742c9d097fca 100644 GIT binary patch literal 1074358 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 BVMqV~ literal 1354811 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 diff --git a/titles/idac/index.py b/titles/idac/index.py index 29ecb67..fb3890d 100644 --- a/titles/idac/index.py +++ b/titles/idac/index.py @@ -6,7 +6,7 @@ import logging import coloredlogs from os import path -from typing import Dict, Tuple +from typing import Dict, List, Tuple from logging.handlers import TimedRotatingFileHandler from twisted.web import server from twisted.web.http import Request @@ -26,9 +26,10 @@ class IDACServlet: def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: self.core_cfg = core_cfg self.game_cfg = IDACConfig() - self.game_cfg.update( - yaml.safe_load(open(f"{cfg_dir}/{IDACConstants.CONFIG_NAME}")) - ) + if path.exists(f"{cfg_dir}/{IDACConstants.CONFIG_NAME}"): + self.game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{IDACConstants.CONFIG_NAME}")) + ) self.versions = [ IDACBase(core_cfg, self.game_cfg), @@ -59,9 +60,7 @@ class IDACServlet: ) @classmethod - def get_allnet_info( - cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str - ) -> Tuple[bool, str, str]: + def is_game_enabled(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> bool: game_cfg = IDACConfig() if path.exists(f"{cfg_dir}/{IDACConstants.CONFIG_NAME}"): @@ -70,28 +69,34 @@ class IDACServlet: ) if not game_cfg.server.enable: - return (False, "", "") - - if core_cfg.server.is_develop: - return ( - True, - f"", - # requires http or else it defautls to https - f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/", - ) + return False + + return True + def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: return ( - True, - f"", - # requires http or else it defautls to https - f"http://{core_cfg.title.hostname}/{game_code}/$v/", + [], + [("render_POST", "/SDGT/{version}/initiald/{category}/{endpoint}", {})] ) - def render_POST(self, request: Request, version: int, url_path: str) -> bytes: + def get_allnet_info( + self, game_code: str, game_ver: int, keychip: str + ) -> Tuple[bool, str, str]: + title_port_int = Utils.get_title_port(self.core_cfg) + t_port = f":{title_port_int}" if title_port_int != 80 and not self.core_cfg.server.is_using_proxy else "" + + return ( + f"", + # requires http or else it defaults to https + f"http://{self.core_cfg.title.hostname}{t_port}/{game_code}/{game_ver}/", + ) + + def render_POST(self, request: Request, game_code: int, matchers: Dict) -> bytes: req_raw = request.content.getvalue() - url_split = url_path.split("/") internal_ver = 0 - endpoint = url_split[len(url_split) - 1] + version = int(matchers['version']) + category = matchers['category'] + endpoint = matchers['endpoint'] client_ip = Utils.get_ip_addr(request) if version >= 100 and version < 140: # IDAC Season 1 @@ -99,45 +104,39 @@ class IDACServlet: elif version >= 140 and version < 171: # IDAC Season 2 internal_ver = IDACConstants.VER_IDAC_SEASON_2 - if url_split[0] == "initiald": - header_application = self.decode_header(request.getAllHeaders()) + header_application = self.decode_header(request.getAllHeaders()) - req_data = json.loads(req_raw) + req_data = json.loads(req_raw) - self.logger.info(f"v{version} {endpoint} request from {client_ip}") - self.logger.debug(f"Headers: {header_application}") - self.logger.debug(req_data) + self.logger.info(f"v{version} {endpoint} request from {client_ip}") + self.logger.debug(f"Headers: {header_application}") + self.logger.debug(req_data) - # func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" - func_to_find = "handle_" - for x in url_split: - func_to_find += f"{x.lower()}_" if not x == "" and not x == "initiald" else "" - func_to_find += f"request" + # func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" + func_to_find = "handle_" + func_to_find += f"{category.lower()}_" if not category == "" else "" + func_to_find += f"{endpoint.lower()}_request" - if not hasattr(self.versions[internal_ver], func_to_find): - self.logger.warning(f"Unhandled v{version} request {endpoint}") - return '{"status_code": "0"}'.encode("utf-8") + if not hasattr(self.versions[internal_ver], func_to_find): + self.logger.warning(f"Unhandled v{version} request {endpoint}") + return '{"status_code": "0"}'.encode("utf-8") - resp = None - try: - handler = getattr(self.versions[internal_ver], func_to_find) - resp = handler(req_data, header_application) + resp = None + try: + handler = getattr(self.versions[internal_ver], func_to_find) + resp = handler(req_data, header_application) - except Exception as e: - traceback.print_exc() - self.logger.error(f"Error handling v{version} method {endpoint} - {e}") - return '{"status_code": "0"}'.encode("utf-8") + except Exception as e: + traceback.print_exc() + self.logger.error(f"Error handling v{version} method {endpoint} - {e}") + return '{"status_code": "0"}'.encode("utf-8") - if resp is None: - resp = {"status_code": "0"} + if resp is None: + resp = {"status_code": "0"} - self.logger.debug(f"Response {resp}") - return json.dumps(resp, ensure_ascii=False).encode("utf-8") + self.logger.debug(f"Response {resp}") + return json.dumps(resp, ensure_ascii=False).encode("utf-8") - self.logger.warning( - f"IDAC unknown request {url_path} - {request.content.getvalue().decode()}" - ) - return '{"status_code": "0"}'.encode("utf-8") def decode_header(self, data: Dict) -> Dict: app: str = data[b"application"].decode() From d1a7b898a734025b2034c9bf278b173447156120 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Mon, 13 Nov 2023 17:32:39 +0100 Subject: [PATCH 072/101] idac: hotfix for title port and changelog --- changelog.md | 14 +++++++------- titles/idac/index.py | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/changelog.md b/changelog.md index d33d08c..9ad97c5 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,13 @@ # Changelog Documenting updates to ARTEMiS, to be updated every time the master branch is pushed to. +## 20231015 +### maimai DX ++ Added support for FESTiVAL PLUS + +### Card Maker ++ Added support for maimai DX FESTiVAL PLUS + ## 20231001 ### Initial D THE ARCADE + Added support for Initial D THE ARCADE S2 @@ -13,13 +20,6 @@ Documenting updates to ARTEMiS, to be updated every time the master branch is pu + Frontend to download profile added + Importer to import profiles added -## 20231015 -### maimai DX -+ Added support for FESTiVAL PLUS - -### Card Maker -+ Added support for maimai DX FESTiVAL PLUS - ## 20230716 ### General + Docker files added (#19) diff --git a/titles/idac/index.py b/titles/idac/index.py index fb3890d..3080f50 100644 --- a/titles/idac/index.py +++ b/titles/idac/index.py @@ -83,7 +83,7 @@ class IDACServlet: self, game_code: str, game_ver: int, keychip: str ) -> Tuple[bool, str, str]: title_port_int = Utils.get_title_port(self.core_cfg) - t_port = f":{title_port_int}" if title_port_int != 80 and not self.core_cfg.server.is_using_proxy else "" + t_port = f":{title_port_int}" if title_port_int and not self.core_cfg.server.is_using_proxy else "" return ( f"", From b19288fb7ef45e8beb81ee9d44914db4b29b05f4 Mon Sep 17 00:00:00 2001 From: Midorica Date: Mon, 13 Nov 2023 12:06:14 -0500 Subject: [PATCH 073/101] idac: removing public urls --- .../data/stamps/touhou_flandre_scarlet.json | Bin 10736 -> 10683 bytes .../data/stamps/touhou_remilia_scarlet.json | Bin 10694 -> 10642 bytes .../data/stamps/touhou_sakuya_izayoi.json | Bin 10683 -> 10631 bytes .../timetrial/touhou_flandre_scarlet.json | Bin 1163 -> 1111 bytes .../timetrial/touhou_remilia_scarlet.json | Bin 1157 -> 1105 bytes .../data/timetrial/touhou_sakuya_izayoi.json | Bin 1139 -> 1087 bytes 6 files changed, 0 insertions(+), 0 deletions(-) diff --git a/titles/idac/data/stamps/touhou_flandre_scarlet.json b/titles/idac/data/stamps/touhou_flandre_scarlet.json index da642f659648e2a507afbf9781cff478e7a22a53..a286a87ecd7a6ff60026c9ea084c2dc532e851c5 100644 GIT binary patch delta 10 RcmewmygPV8!N#TDngAbk1snhX delta 58 zcmdlT{2_QkfqrRGj*^vvQbtKhL9vy-er8@;zHVk-W=UpZPD*03UU6!AqFz>kzLAlo NfxeQC!p5vXO#p=76f6J$ diff --git a/titles/idac/data/stamps/touhou_remilia_scarlet.json b/titles/idac/data/stamps/touhou_remilia_scarlet.json index 93369e8a86d46f0591fd8bdd975fd459f849b16c..d3a1b4250c9f705c4e2c93673a0ac4f4289e094e 100644 GIT binary patch delta 10 RcmX>WJSliW*2Z}nngARR1epK; delta 55 zcmbOfd@OiEmQHC=j*^vvQbtKhL9vy-er8@;zHVk-W=UpZPD*03UU6!AqFz>kzL9~Y Knf}Hk15E%pNE1B( diff --git a/titles/idac/data/stamps/touhou_sakuya_izayoi.json b/titles/idac/data/stamps/touhou_sakuya_izayoi.json index 05d1e2877f3573663d16d43687d853f400f90978..3749eee9ebfe0362e1bf1cdc9c8830a77944aca6 100644 GIT binary patch delta 10 RcmdlT+#Wn3Vq=fKCIA}81ZDsL delta 55 zcmZn<-W@z4LZ`GSN6AV-DWjyMpx8=ZKQk{aUpF%^vm`SyCnYgiuQ)Y5Q7@}N-^kd& KOn+m5xh4QF(Gvs! diff --git a/titles/idac/data/timetrial/touhou_flandre_scarlet.json b/titles/idac/data/timetrial/touhou_flandre_scarlet.json index e7bab3d4bbf589aa4fbc1b7f042b054cd5c08455..0db5ed7f20184e150ce90d79b7ca5a49a734458b 100644 GIT binary patch delta 10 RcmeC?yv{KpdSm||W&jvW1fBo@ delta 55 zcmcc4(akv_TBo!qN6AV-DWjyMpx8=ZKQk{aUpF%^vm`SyCnYgiuQ)Y5Q7@}N-^ke1 KOn+lA3kv`kZW2NO diff --git a/titles/idac/data/timetrial/touhou_remilia_scarlet.json b/titles/idac/data/timetrial/touhou_remilia_scarlet.json index 2414f4a125e537b8f102c07dea83bdb7300084ae..6b50a2b126612a35e9dc88688453439ba57103c8 100644 GIT binary patch delta 10 RcmZqWyvQ*jbYtfqW&jtk1cm?r delta 55 zcmcb}(aJd?RHw8kN6AV-DWjyMpx8=ZKQk{aUpF%^vm`SyCnYgiuQ)Y5Q7@}N-^kd+ KRDYu{3kv`gn-VAh diff --git a/titles/idac/data/timetrial/touhou_sakuya_izayoi.json b/titles/idac/data/timetrial/touhou_sakuya_izayoi.json index b817f5edfb0450c7f69d1fb602c8780881cfbb31..c31d3911e492267e95b29d703ab90088dfc0ceaf 100644 GIT binary patch delta 10 Rcmey&v7cjt^TzT&%m5nG1ib(N delta 55 zcmdnb@tI?SvrcJIj*^vvQbtKhL9vy-er8@;zHVk-W=UpZPD*03UU6!AqFz>kzLAN6 Kk^V+&78U>_ZxUGm From 2c275d1130d243d1d3fa26e0a161ed0f60693167 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Mon, 13 Nov 2023 12:14:34 -0500 Subject: [PATCH 074/101] idac: add logging to setup --- titles/idac/index.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/titles/idac/index.py b/titles/idac/index.py index 3080f50..7daedae 100644 --- a/titles/idac/index.py +++ b/titles/idac/index.py @@ -163,3 +163,5 @@ class IDACServlet: self.game_cfg.server.echo2, IDACEchoUDP(self.core_cfg, self.game_cfg, self.game_cfg.server.echo2), ) + + self.logger.info(f"Matching listening on {self.game_cfg.server.matching} with echos on {self.game_cfg.server.echo1} and {self.game_cfg.server.echo2}") From ee45c55f66d29244ef082ad1e80b0d2ff7be1fcc Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Mon, 13 Nov 2023 12:53:40 -0500 Subject: [PATCH 075/101] sao: stub get_shop_resource_sales_data_list --- titles/sao/base.py | 7 ++ titles/sao/handlers/base.py | 21 ++++ titles/sao/handlers/helpers.py | 202 ++++++++++++++++++++++++++++++++- 3 files changed, 228 insertions(+), 2 deletions(-) diff --git a/titles/sao/base.py b/titles/sao/base.py index 91bdd1a..d1b8f96 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -831,4 +831,11 @@ class SaoBase: def handle_c306(self, header: SaoRequestHeader, request: bytes) -> bytes: #card/scan_qr_quest_profile_card resp = SaoScanQrQuestProfileCardResponse(header.cmd +1) + return resp.make() + + def handle_c700(self, header: SaoRequestHeader, request: bytes) -> bytes: + # shop/get_shop_resource_sales_data_list + # TODO: Shop data + req = GetShopResourceSalesDataListRequest(header, request) + resp = GetShopResourceSalesDataListResponse(header.cmd + 1) return resp.make() \ No newline at end of file diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py index 3c20e80..bd93fbb 100644 --- a/titles/sao/handlers/base.py +++ b/titles/sao/handlers/base.py @@ -3095,3 +3095,24 @@ class TrialTowerPlayEndUnanalyzedLogFixed(SaoBaseRequest): self.rarity_up_exec_flag = decode_byte(data, off) off += BYTE_OFF + +class GetShopResourceSalesDataListRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + user_id = decode_str(data, off) + self.user_id = user_id[0] + off += user_id[1] + +class GetShopResourceSalesDataListResponse(SaoBaseResponse): + def __init__(self, cmd_id: int) -> None: + super().__init__(cmd_id) + self.result = 1 # byte + self.shop_resource_sales_data: List[ShopResourceSalesData] = [] + + def make(self) -> bytes: + ret = encode_byte(self.result) + ret += encode_arr_cls(self.shop_resource_sales_data) + + self.header.length = len(ret) + return super().make() + ret \ No newline at end of file diff --git a/titles/sao/handlers/helpers.py b/titles/sao/handlers/helpers.py index ba91751..8123a18 100644 --- a/titles/sao/handlers/helpers.py +++ b/titles/sao/handlers/helpers.py @@ -1,6 +1,7 @@ -from typing import Tuple, List +from typing import Tuple, List, Optional import struct import logging +from datetime import datetime BIGINT_OFF = 16 LONG_OFF = 8 @@ -8,6 +9,18 @@ INT_OFF = 4 SHORT_OFF = 2 BYTE_OFF = 1 +DT_FMT = "%Y%m%d%H%M%S" + +def fmt_dt(d: Optional[datetime] = None) -> str: + if d is None: + d = datetime.fromtimestamp(0) + return d.strftime(DT_FMT) + +def prs_dt(s: Optional[str] = None) -> datetime: + if not s: + s = "19691231190000" + return datetime.strptime(s, DT_FMT) + def decode_num(data: bytes, offset: int, size: int) -> int: try: return int.from_bytes(data[offset:offset + size], 'big') @@ -40,6 +53,31 @@ def decode_str(data: bytes, offset: int) -> Tuple[str, int]: logging.getLogger('sao').error(f"Failed to parse {data[offset:]} as string!") return ("", 0) +def decode_arr_num(data: bytes, offset:int, element_size: int) -> Tuple[List[int], int]: + size = 0 + num_obj = decode_int(data, offset + size) + size += INT_OFF + + ret: List[int] = [] + for _ in range(num_obj): + ret.append(decode_num(data, offset + size, element_size)) + size += element_size + + return (ret, size) + +def decode_arr_str(data: bytes, offset: int) -> Tuple[List[str], int]: + size = 0 + num_obj = decode_int(data, offset + size) + size += INT_OFF + + ret: List[str] = [] + for _ in range(num_obj): + tmp = decode_str(data, offset + size) + ret.append(tmp[0]) + size += tmp[1] + + return (ret, size) + def encode_byte(data: int) -> bytes: return struct.pack("!B", data) @@ -63,14 +101,66 @@ def encode_str(s: str) -> bytes: except: logging.getLogger('sao').error(f"Failed to encode {s} as bytes!") return b"" - + +def encode_arr_num(data: List[int], element_size: int) -> bytes: + ret = encode_int(len(data)) + + if element_size == BYTE_OFF: + for x in data: + ret += encode_byte(x) + elif element_size == SHORT_OFF: + for x in data: + ret += encode_short(x) + elif element_size == INT_OFF: + for x in data: + ret += encode_int(x) + elif element_size == LONG_OFF: + for x in data: + ret += encode_long(x) + elif element_size == BIGINT_OFF: + for x in data: + ret += encode_bigint(x) + else: + logging.getLogger('sao').error(f"Unknown element size {element_size}") + return b"\x00" * INT_OFF + + return ret + class BaseHelper: def __init__(self, data: bytes, offset: int) -> None: self._sz = 0 + + @classmethod + def from_args(cls) -> "BaseHelper": + return cls(b"", 0) def get_size(self) -> int: return self._sz + def make(self) -> bytes: + return b"" + +def decode_arr_cls(data: bytes, offset: int, cls: BaseHelper): + size = 0 + num_cls = decode_int(data, offset + size) + cls_type = type(cls) + + ret: List[cls_type] = [] + for _ in range(num_cls): + tmp = cls(data, offset + size) + size += tmp.get_size() + ret.append(tmp) + + return (ret, size) + +def encode_arr_cls(data: List[BaseHelper]) -> bytes: + ret = encode_int(len(data)) + + for x in data: + ret += x.make() + + return ret + class MaterialCommonRewardUserData(BaseHelper): def __init__(self, data: bytes, offset: int) -> None: super().__init__(data, offset) @@ -413,3 +503,111 @@ class MultiPlayEndRequestData(BaseHelper): self._sz += BYTE_OFF self.dummy_3 = decode_byte(data, offset + self._sz) self._sz += BYTE_OFF + +class SalesResourceData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + self.common_reward_type = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + self.common_reward_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + self.property1_property_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.property1_value1 = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.property1_value2 = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + self.property2_property_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.property2_value1 = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.property2_value2 = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + self.property3_property_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.property3_value1 = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.property3_value2 = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + self.property4_property_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.property4_value1 = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.property4_value2 = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + @classmethod + def from_args(cls, reward_type: int = 0, reward_id: int = 0) -> "SalesResourceData": + ret = cls(b"\x00" * 54, 0) + ret.common_reward_type = reward_type # short + ret.common_reward_id = reward_id # int + + return ret + + def make(self) -> bytes: + ret = b"" + ret += encode_short(self.common_reward_type) + ret += encode_int(self.common_reward_id) + + ret += encode_int(self.property1_property_id) + ret += encode_int(self.property1_value1) + ret += encode_int(self.property1_value2) + + ret += encode_int(self.property2_property_id) + ret += encode_int(self.property2_value1) + ret += encode_int(self.property2_value2) + + ret += encode_int(self.property3_property_id) + ret += encode_int(self.property3_value1) + ret += encode_int(self.property3_value2) + + ret += encode_int(self.property4_property_id) + ret += encode_int(self.property4_value1) + ret += encode_int(self.property4_value2) + +class ShopResourceSalesData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + user_shop_resource_id = decode_str(data, offset + self._sz) + self.user_shop_resource_id = user_shop_resource_id[0] + self._sz = user_shop_resource_id[1] + + discharge_user_id = decode_str(data, offset + self._sz) + self.discharge_user_id = discharge_user_id[0] + self._sz = discharge_user_id[1] + + self.remaining_num = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + self.purchase_num = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + + sales_start_date = decode_str(data, offset + self._sz) + self.sales_start_date = prs_dt(sales_start_date[0]) + self._sz = sales_start_date[1] + + sales_resource_data_list = decode_arr_cls(data, offset + self._sz, SalesResourceData) + self.sales_resource_data_list: List[SalesResourceData] = sales_resource_data_list[0] + self._sz += sales_resource_data_list[1] + + @classmethod + def from_args(cls, resource_id: str = "0", discharge_id: int = "0", remaining: int = 0, purchased: int = 0) -> "ShopResourceSalesData": + ret = cls(b"\x00" * 20, 0) + ret.user_shop_resource_id = resource_id + ret.discharge_user_id = discharge_id + ret.remaining_num = remaining # short + ret.purchase_num = purchased # short + ret.sales_start_date = prs_dt() + + def make(self) -> bytes: + ret = encode_str(self.user_shop_resource_id) + ret += encode_str(self.discharge_user_id) + ret += encode_short(self.remaining_num) + ret += encode_short(self.purchase_num) + ret += encode_str(fmt_dt(self.sales_start_date)) + ret += encode_arr_cls(self.sales_resource_data_list) + return ret + \ No newline at end of file From 22cf3f83a69c6d6c4d5a258a93e0bcf3dc21499f Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Mon, 13 Nov 2023 13:47:17 -0500 Subject: [PATCH 076/101] remove randbytes --- titles/idac/echo.py | 1 - titles/sao/index.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/titles/idac/echo.py b/titles/idac/echo.py index 88151a7..08e5526 100644 --- a/titles/idac/echo.py +++ b/titles/idac/echo.py @@ -1,5 +1,4 @@ import logging -from random import randbytes import socket from twisted.internet.protocol import DatagramProtocol diff --git a/titles/sao/index.py b/titles/sao/index.py index 5bbdc7e..be7c3cf 100644 --- a/titles/sao/index.py +++ b/titles/sao/index.py @@ -6,7 +6,7 @@ from logging.handlers import TimedRotatingFileHandler from os import path from Crypto.Cipher import Blowfish from hashlib import md5 -import random +import secrets from core import CoreConfig, Utils from core.title import BaseServlet @@ -123,7 +123,7 @@ class SaoServlet(BaseServlet): self.logger.debug(f"Response: {resp.hex()}") if self.game_cfg.crypt.enable: - iv = random.randbytes(8) + iv = secrets.token_bytes(8) data_to_crypt = resp[24:] while len(data_to_crypt) % 8 != 0: data_to_crypt += b"\x00" From a9f72cec6995516c65c1ce80b26e4807250ab148 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Mon, 13 Nov 2023 14:45:20 -0500 Subject: [PATCH 077/101] sao: add shop data tables --- titles/sao/data/GashaMedalShopItems.csv | Bin 0 -> 46920 bytes titles/sao/data/GashaMedalShops.csv | Bin 0 -> 84827 bytes titles/sao/data/ResEarnCampaignShopItems.csv | Bin 0 -> 633 bytes titles/sao/data/ResEarnCampaignShops.csv | Bin 0 -> 819 bytes titles/sao/data/YuiMedalShopItems.csv | Bin 0 -> 41171 bytes titles/sao/data/YuiMedalShops.csv | Bin 0 -> 94753 bytes 6 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 titles/sao/data/GashaMedalShopItems.csv create mode 100644 titles/sao/data/GashaMedalShops.csv create mode 100644 titles/sao/data/ResEarnCampaignShopItems.csv create mode 100644 titles/sao/data/ResEarnCampaignShops.csv create mode 100644 titles/sao/data/YuiMedalShopItems.csv create mode 100644 titles/sao/data/YuiMedalShops.csv diff --git a/titles/sao/data/GashaMedalShopItems.csv b/titles/sao/data/GashaMedalShopItems.csv new file mode 100644 index 0000000000000000000000000000000000000000..34b4504f6ed996c2be1dcd5b35c584226207db30 GIT binary patch literal 46920 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 literal 0 HcmV?d00001 diff --git a/titles/sao/data/GashaMedalShops.csv b/titles/sao/data/GashaMedalShops.csv new file mode 100644 index 0000000000000000000000000000000000000000..1f0e2e598fe3898c774f68a516f247add6c0dcc6 GIT binary patch literal 84827 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 literal 0 HcmV?d00001 diff --git a/titles/sao/data/ResEarnCampaignShopItems.csv b/titles/sao/data/ResEarnCampaignShopItems.csv new file mode 100644 index 0000000000000000000000000000000000000000..83810b69d4f2869772f131aeb1205618773f3370 GIT binary patch literal 633 zcmaiv&2GXl5QOhMMIT@#cGsaKXT*U^g#dB46bmZhBr-u2Z_lnx_;I85;E~7nw=!`Mv?i`M(L*|;AY>fN<-ZwErX-hW*@Z2RDvLw|&I zXge4BD-9cYJZx!iPZvj-=ZELv;S0kVll+r0BaF$IkJWpsgM}oToAt53& z0zq1XvUr53k>SXQ3d72n{}~e*BU4fiVij{33z-13N922084IeGz@%X~5~hXkWu;(= aT2NJ(_$mDST`*^~V2MVVYOXoe-^>luM6rDU literal 0 HcmV?d00001 diff --git a/titles/sao/data/ResEarnCampaignShops.csv b/titles/sao/data/ResEarnCampaignShops.csv new file mode 100644 index 0000000000000000000000000000000000000000..c52c00db992fb0a4e7d4d67387adbc14c86da9a0 GIT binary patch literal 819 zcmbW#O-lkn7zglspF%uO?Ch?lbPJOZN+eGmha7aleL=_0>yDBy6jX(gWme*Vf)y-bSyUvki?OI^*icX#RCzCeArEIGHQw_UgO^5O|ZMbUjugtjvHFB|6@vQ zb#zI*7@feTgXgLH;A4G9cs{oNg%mXw-<(>G&hGql10nnw0!mEs4fr`YPkun&TcnHc HY=-^bi> literal 0 HcmV?d00001 diff --git a/titles/sao/data/YuiMedalShopItems.csv b/titles/sao/data/YuiMedalShopItems.csv new file mode 100644 index 0000000000000000000000000000000000000000..0ce915711b0cb45efefaf5a3fe1c775ce265f22b GIT binary patch literal 41171 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 C>9^JZ literal 0 HcmV?d00001 diff --git a/titles/sao/data/YuiMedalShops.csv b/titles/sao/data/YuiMedalShops.csv new file mode 100644 index 0000000000000000000000000000000000000000..3c0cfb9700ad7f7a7a56cd8883d67332fcd6f168 GIT binary patch literal 94753 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 Date: Mon, 13 Nov 2023 17:17:27 -0500 Subject: [PATCH 078/101] sao: fix yui shops --- titles/sao/base.py | 98 +++++++++- titles/sao/data/GashaMedalShopItems.csv | Bin 46920 -> 46917 bytes titles/sao/data/GashaMedalShops.csv | Bin 84827 -> 84824 bytes titles/sao/data/ResEarnCampaignShopItems.csv | Bin 633 -> 630 bytes titles/sao/data/ResEarnCampaignShops.csv | Bin 819 -> 816 bytes titles/sao/data/YuiMedalShopItems.csv | Bin 41171 -> 41168 bytes titles/sao/data/YuiMedalShops.csv | Bin 94753 -> 94750 bytes titles/sao/handlers/base.py | 82 +++++++- titles/sao/handlers/helpers.py | 193 ++++++++++++++++++- 9 files changed, 367 insertions(+), 6 deletions(-) diff --git a/titles/sao/base.py b/titles/sao/base.py index d1b8f96..2470e7e 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -1,6 +1,8 @@ import logging from csv import * from random import choice +from typing import Dict, List +from os import path from core.data import Data from core import CoreConfig @@ -17,6 +19,19 @@ class SaoBase: self.version = 0 self.logger = logging.getLogger("sao") + def load_data_csv(self, file: str) -> List[Dict]: + ret = [] + if path.exists(f"titles/sao/data/{file}.csv"): + with open(f"titles/sao/data/{file}.csv", "r", encoding="utf8") as f: + data = csv.DictReader(f, delimiter=',') + for x in data: + ret.append(x) + + return ret + + self.logger.warning(f"Failed to find csv file {file}.csv") + return ret + def handle_noop(self, header: SaoRequestHeader, request: bytes) -> bytes: self.logger.info(f"Using Generic handler") resp_thing = SaoNoopResponse(header.cmd + 1) @@ -835,7 +850,86 @@ class SaoBase: def handle_c700(self, header: SaoRequestHeader, request: bytes) -> bytes: # shop/get_shop_resource_sales_data_list - # TODO: Shop data + # TODO: Get user shop data req = GetShopResourceSalesDataListRequest(header, request) resp = GetShopResourceSalesDataListResponse(header.cmd + 1) - return resp.make() \ No newline at end of file + return resp.make() + + def handle_d100(self, header: SaoRequestHeader, request: bytes) -> bytes: + # shop/get_yui_medal_shop_user_data_list + # TODO: Get user shop data + req = GetYuiMedalShopUserDataListRequest(header, request) + resp = GetYuiMedalShopUserDataListResponse(header.cmd + 1) + return resp.make() + + def handle_cf0e(self, header: SaoRequestHeader, request: bytes) -> bytes: + # gasha/get_gasha_medal_shop_user_data_list + # TODO: Get user shop data + req = GetGashaMedalShopUserDataListRequest(header, request) + resp = GetGashaMedalShopUserDataListResponse(header.cmd + 1) + return resp.make() + + def handle_d5da(self, header: SaoRequestHeader, request: bytes) -> bytes: + # master_data/get_m_yui_medal_shops + req = GetMYuiMedalShopDataRequest(header, request) + resp = GetMYuiMedalShopDataResponse(header.cmd + 1) + + shops = self.load_data_csv("YuiMedalShops") + for shop in shops: + tmp = YuiMedalShopData.from_args(int(shop['YuiMedalShopId']), shop['Name'], shop['Description']) + tmp.selling_yui_medal = int(shop['SellingYuiMedal']) + tmp.selling_col = int(shop['SellingCol']) + tmp.selling_event_item_id = int(shop['SellingEventItemId']) + tmp.selling_event_item_num = int(shop['SellingEventItemNum']) + tmp.selling_ticket_num = int(shop['SellingTicketNum']) + tmp.purchase_limit = int(shop['PurchaseLimit']) + tmp.pick_up_flag = 1 if shop['PickUpFlag'] == "True" else 0 + tmp.product_category = int(shop['ProductCategory']) + tmp.sales_type = int(shop['SalesType']) + tmp.target_days = int(shop['TargetDays']) + tmp.target_hour = int(shop['TargetHour']) + tmp.interval_hour = int(shop['IntervalHour']) + tmp.sort = int(shop['Sort']) + + tmp.sales_end_date = datetime(2100, 1, 1, 0, 0, 0, 0) # always open + + resp.data_list.append(tmp) + + return resp.make() + + def handle_d5dc(self, header: SaoRequestHeader, request: bytes) -> bytes: + # master_data/get_m_yui_medal_shop_items + req = GetMYuiMedalShopItemsRequest(header, request) + resp = GetMYuiMedalShopItemsResponse(header.cmd + 1) + + shops = self.load_data_csv("YuiMedalShopItems") + for shop in shops: + tmp = YuiMedalShopItemData.from_args(int(shop['YuiMedalShopItemId']), int(shop['YuiMedalShopId']), int(shop['CommonRewardType']), int(shop['CommonRewardId']), int(shop['CommonRewardNum']), int(shop['Strength'])) + + tmp.property1_property_id = int(shop['Property1PropertyId']) + tmp.property1_value1 = int(shop['Property1Value1']) + tmp.property1_value2 = int(shop['Property1Value2']) + + tmp.property2_property_id = int(shop['Property2PropertyId']) + tmp.property2_value1 = int(shop['Property2Value1']) + tmp.property2_value2 = int(shop['Property2Value2']) + + tmp.property3_property_id = int(shop['Property3PropertyId']) + tmp.property3_value1 = int(shop['Property3Value1']) + tmp.property3_value2 = int(shop['Property3Value2']) + + tmp.property4_property_id = int(shop['Property4PropertyId']) + tmp.property4_value1 = int(shop['Property4Value1']) + tmp.property4_value2 = int(shop['Property4Value2']) + + resp.data_list.append(tmp) + + return resp.make() + + def handle_d5fc(self, header: SaoRequestHeader, request: bytes) -> bytes: + # master_data/get_m_gasha_medal_shops + pass + + def handle_d604(self, header: SaoRequestHeader, request: bytes) -> bytes: + # master_data_2/get_m_res_earn_campaign_shops + pass \ No newline at end of file diff --git a/titles/sao/data/GashaMedalShopItems.csv b/titles/sao/data/GashaMedalShopItems.csv index 34b4504f6ed996c2be1dcd5b35c584226207db30..c5a46aef3f4ad02b57b160fee878347396bab3f8 100644 GIT binary patch delta 10 RcmX@{j_K$-rj5+5+W{X}1s(tZ delta 13 UcmX^5j_JfZCT4wog$=IT0V{O{n*aa+ diff --git a/titles/sao/data/GashaMedalShops.csv b/titles/sao/data/GashaMedalShops.csv index 1f0e2e598fe3898c774f68a516f247add6c0dcc6..a4a78dcf5656b77f98263f9f031cf7f0a88de8ed 100644 GIT binary patch delta 13 UcmcaTjrGPf)<)*7%#0D<04Q|?$^ZZW delta 15 WcmcaHjrI04R%U&Dg@!GR5#9hV{slt- diff --git a/titles/sao/data/ResEarnCampaignShopItems.csv b/titles/sao/data/ResEarnCampaignShopItems.csv index 83810b69d4f2869772f131aeb1205618773f3370..6d5dd17081410e8e3fd8905604d50d3ef5d88f29 100644 GIT binary patch delta 8 Pcmey#@{MI9a~Tr=5q|?D delta 11 Scmeyy@{@&GUteKE84~~*J_Dfu diff --git a/titles/sao/data/ResEarnCampaignShops.csv b/titles/sao/data/ResEarnCampaignShops.csv index c52c00db992fb0a4e7d4d67387adbc14c86da9a0..2c201c9cc9334e352f2dcc01f6dfb1d64af99b1a 100644 GIT binary patch delta 8 PcmdnYwt;OUvjH;z4LAZ< delta 11 ScmdnMwwaAtUteK^0W$y-Ujozs diff --git a/titles/sao/data/YuiMedalShopItems.csv b/titles/sao/data/YuiMedalShopItems.csv index 0ce915711b0cb45efefaf5a3fe1c775ce265f22b..f5d91fe049abf8002ed086280ccf974c08008f5b 100644 GIT binary patch delta 10 Scmcb7kmS4CT4wog$)-L001e#1c diff --git a/titles/sao/data/YuiMedalShops.csv b/titles/sao/data/YuiMedalShops.csv index 3c0cfb9700ad7f7a7a56cd8883d67332fcd6f168..5bac235cf3b967a4e7f196f27e9f91d9a9e5401b 100644 GIT binary patch delta 13 UcmZ4ZgmvB%)<)*7%#3nH04Z1m-v9sr delta 15 WcmbRDgmvK)R%U&Dg@!GRazy|!F$Guv diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py index bd93fbb..426ca5f 100644 --- a/titles/sao/handlers/base.py +++ b/titles/sao/handlers/base.py @@ -3103,7 +3103,7 @@ class GetShopResourceSalesDataListRequest(SaoBaseRequest): user_id = decode_str(data, off) self.user_id = user_id[0] off += user_id[1] - + class GetShopResourceSalesDataListResponse(SaoBaseResponse): def __init__(self, cmd_id: int) -> None: super().__init__(cmd_id) @@ -3115,4 +3115,82 @@ class GetShopResourceSalesDataListResponse(SaoBaseResponse): ret += encode_arr_cls(self.shop_resource_sales_data) self.header.length = len(ret) - return super().make() + ret \ No newline at end of file + return super().make() + ret + +class GetYuiMedalShopUserDataListRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + user_id = decode_str(data, off) + self.user_id = user_id[0] + off += user_id[1] + +class GetYuiMedalShopUserDataListResponse(SaoBaseResponse): + def __init__(self, cmd_id: int) -> None: + super().__init__(cmd_id) + self.result = 1 # byte + self.user_data_list: List[YuiMedalShopUserData] = [] + + def make(self) -> bytes: + ret = encode_byte(self.result) + ret += encode_arr_cls(self.user_data_list) + + self.header.length = len(ret) + return super().make() + ret + +class GetGashaMedalShopUserDataListRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + user_id = decode_str(data, off) + self.user_id = user_id[0] + off += user_id[1] + +class GetGashaMedalShopUserDataListResponse(SaoBaseResponse): + def __init__(self, cmd_id: int) -> None: + super().__init__(cmd_id) + self.result = 1 # byte + self.data_list: List[GashaMedalShopUserData] = [] + + def make(self) -> bytes: + ret = encode_byte(self.result) + ret += encode_arr_cls(self.data_list) + + self.header.length = len(ret) + return super().make() + ret + +class GetMYuiMedalShopDataRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + self.dummy = decode_byte(data, 0) + +class GetMYuiMedalShopDataResponse(SaoBaseResponse): + def __init__(self, cmd_id: int) -> None: + super().__init__(cmd_id) + self.result = 1 # byte + self.data_list: List[YuiMedalShopData] = [] + + def make(self) -> bytes: + ret = encode_byte(self.result) + ret += encode_arr_cls(self.data_list) + + self.header.length = len(ret) + return super().make() + ret + +class GetMYuiMedalShopItemsRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + self.dummy = decode_byte(data, 0) + +class GetMYuiMedalShopItemsResponse(SaoBaseResponse): + def __init__(self, cmd_id: int) -> None: + super().__init__(cmd_id) + self.result = 1 # byte + self.data_list: List[YuiMedalShopItemData] = [] + + def make(self) -> bytes: + ret = encode_byte(self.result) + ret += encode_arr_cls(self.data_list) + + self.header.length = len(ret) + return super().make() + ret diff --git a/titles/sao/handlers/helpers.py b/titles/sao/handlers/helpers.py index 8123a18..9ad26f0 100644 --- a/titles/sao/handlers/helpers.py +++ b/titles/sao/handlers/helpers.py @@ -594,7 +594,7 @@ class ShopResourceSalesData(BaseHelper): self._sz += sales_resource_data_list[1] @classmethod - def from_args(cls, resource_id: str = "0", discharge_id: int = "0", remaining: int = 0, purchased: int = 0) -> "ShopResourceSalesData": + def from_args(cls, resource_id: str = "0", discharge_id: str = "0", remaining: int = 0, purchased: int = 0) -> "ShopResourceSalesData": ret = cls(b"\x00" * 20, 0) ret.user_shop_resource_id = resource_id ret.discharge_user_id = discharge_id @@ -610,4 +610,193 @@ class ShopResourceSalesData(BaseHelper): ret += encode_str(fmt_dt(self.sales_start_date)) ret += encode_arr_cls(self.sales_resource_data_list) return ret - \ No newline at end of file + +class YuiMedalShopUserData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + self.yui_medal_shop_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + self.purchase_num = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + last_purchase_date = decode_str(data, offset + self._sz) + self.last_purchase_date = last_purchase_date[0] + self._sz += last_purchase_date[1] + + @classmethod + def from_args(cls, yui_medal_shop_id: int = 0, purchase_num: int = 0, last_purchase_date: datetime = datetime.fromtimestamp(0)) -> "YuiMedalShopUserData": + ret = cls(b"\x00" * 20, 0) + ret.yui_medal_shop_id = yui_medal_shop_id + ret.purchase_num = purchase_num + ret.last_purchase_date = last_purchase_date + + def make(self) -> bytes: + ret = encode_int(self.yui_medal_shop_id) + ret += encode_int(self.purchase_num) + ret += encode_str(fmt_dt(self.last_purchase_date)) + return ret + +class GashaMedalShopUserData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + self.gasha_medal_shop_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + self.purchase_num = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + @classmethod + def from_args(cls, gasha_medal_shop_id: int = 0, purchase_num: int = 0) -> "GashaMedalShopUserData": + ret = cls(b"\x00" * 20, 0) + ret.gasha_medal_shop_id = gasha_medal_shop_id + ret.purchase_num = purchase_num + + def make(self) -> bytes: + ret = encode_int(self.gasha_medal_shop_id) + ret += encode_int(self.purchase_num) + return ret + +class YuiMedalShopData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + self.yui_medal_shop_id = decode_int(data, offset + self._sz) + + name = decode_str(data, offset + self._sz) + self.name = name[0] + self._sz += name[1] + + description = decode_str(data, offset + self._sz) + self.description = description[0] + self._sz += description[1] + + self.selling_yui_medal = decode_short(data, offset + self._sz) + self.selling_col = decode_int(data, offset + self._sz) + self.selling_event_item_id = decode_int(data, offset + self._sz) + self.selling_event_item_num = decode_int(data, offset + self._sz) + self.selling_ticket_num = decode_int(data, offset + self._sz) + self.purchase_limit = decode_short(data, offset + self._sz) + self.pick_up_flag = decode_byte(data, offset + self._sz) + self.product_category = decode_byte(data, offset + self._sz) + self.sales_type = decode_byte(data, offset + self._sz) + self.target_days = decode_byte(data, offset + self._sz) + self.target_hour = decode_byte(data, offset + self._sz) + self.interval_hour = decode_byte(data, offset + self._sz) + + sales_start_date = decode_str(data, offset + self._sz) + self.sales_start_date = prs_dt(sales_start_date[0]) + self._sz += sales_start_date[1] + + sales_end_date = decode_str(data, offset + self._sz) + self.sales_end_date = prs_dt(sales_end_date[0]) + self._sz += sales_end_date[1] + + self.sort = decode_byte(data, offset + self._sz) + + @classmethod + def from_args(cls, shop_id: int = 0, name: str = "", desc: str = "") -> "YuiMedalShopData": + ret = cls(b"\x00" * 43, 0) + ret.yui_medal_shop_id = shop_id + ret.name = name + ret.description = desc + + def make(self) -> bytes: + ret = encode_int(self.yui_medal_shop_id) + ret += encode_str(self.name) + ret += encode_str(self.description) + ret += encode_short(self.selling_yui_medal) + ret += encode_int(self.selling_col) + ret += encode_int(self.selling_event_item_id) + ret += encode_int(self.selling_event_item_num) + ret += encode_int(self.selling_ticket_num) + ret += encode_short(self.purchase_limit) + ret += encode_byte(self.pick_up_flag) + ret += encode_byte(self.product_category) + ret += encode_byte(self.sales_type) + ret += encode_byte(self.target_days) + ret += encode_byte(self.target_hour) + ret += encode_byte(self.interval_hour) + ret += encode_str(self.sales_end_date) + ret += encode_str(self.sales_end_date) + ret += encode_byte(self.sort) + return ret + +class YuiMedalShopItemData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + self.yui_medal_shop_item_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.yui_medal_shop_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.common_reward_type = decode_byte(data, offset + self._sz) + self._sz += BYTE_OFF + self.common_reward_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.common_reward_num = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + self.strength = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + self.property1_property_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.property1_value1 = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.property1_value2 = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + self.property2_property_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.property2_value1 = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.property2_value2 = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + self.property3_property_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.property3_value1 = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.property3_value2 = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + self.property4_property_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.property4_value1 = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.property4_value2 = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + @classmethod + def from_args(cls, item_id: int = 0, shop_id: int = 0, reward_type: int = 0, reward_id: int = 0, reward_num: int = 0, strength: int = 0) -> "YuiMedalShopItemData": + ret = cls(b"\x00" * 67, 0) + ret.yui_medal_shop_item_id = item_id + ret.yui_medal_shop_id = shop_id + ret.common_reward_type = reward_type + ret.common_reward_id = reward_id + ret.common_reward_num = reward_num + ret.strength = strength + return ret + + def make(self) -> bytes: + ret = encode_int(self.yui_medal_shop_item_id) + ret += encode_int(self.yui_medal_shop_id) + ret += encode_byte(self.common_reward_type) + ret += encode_int(self.common_reward_id) + ret += encode_short(self.common_reward_num) + ret += encode_int(self.strength) + + ret += encode_int(self.property1_property_id) + ret += encode_int(self.property1_value1) + ret += encode_int(self.property1_value2) + + ret += encode_int(self.property2_property_id) + ret += encode_int(self.property2_value1) + ret += encode_int(self.property2_value2) + + ret += encode_int(self.property3_property_id) + ret += encode_int(self.property3_value1) + ret += encode_int(self.property3_value2) + + ret += encode_int(self.property4_property_id) + ret += encode_int(self.property4_value1) + ret += encode_int(self.property4_value2) + return ret From a06d1aaf8c12ad53a316f9d720f8fab8f5983b89 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Mon, 13 Nov 2023 17:24:22 -0500 Subject: [PATCH 079/101] sao: fix helpers --- titles/sao/handlers/helpers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/titles/sao/handlers/helpers.py b/titles/sao/handlers/helpers.py index 9ad26f0..f669c7c 100644 --- a/titles/sao/handlers/helpers.py +++ b/titles/sao/handlers/helpers.py @@ -630,6 +630,7 @@ class YuiMedalShopUserData(BaseHelper): ret.yui_medal_shop_id = yui_medal_shop_id ret.purchase_num = purchase_num ret.last_purchase_date = last_purchase_date + return ret def make(self) -> bytes: ret = encode_int(self.yui_medal_shop_id) @@ -651,6 +652,7 @@ class GashaMedalShopUserData(BaseHelper): ret = cls(b"\x00" * 20, 0) ret.gasha_medal_shop_id = gasha_medal_shop_id ret.purchase_num = purchase_num + return ret def make(self) -> bytes: ret = encode_int(self.gasha_medal_shop_id) @@ -699,6 +701,7 @@ class YuiMedalShopData(BaseHelper): ret.yui_medal_shop_id = shop_id ret.name = name ret.description = desc + return ret def make(self) -> bytes: ret = encode_int(self.yui_medal_shop_id) From 25528203fe9b26d834c905087e779f9276d5423f Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Mon, 13 Nov 2023 17:31:23 -0500 Subject: [PATCH 080/101] sao: fix encoder helpers --- titles/sao/handlers/helpers.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/titles/sao/handlers/helpers.py b/titles/sao/handlers/helpers.py index f669c7c..b6426a0 100644 --- a/titles/sao/handlers/helpers.py +++ b/titles/sao/handlers/helpers.py @@ -79,19 +79,19 @@ def decode_arr_str(data: bytes, offset: int) -> Tuple[List[str], int]: return (ret, size) def encode_byte(data: int) -> bytes: - return struct.pack("!B", data) + return struct.pack("!b", data) def encode_short(data: int) -> bytes: - return struct.pack("!H", data) + return struct.pack("!h", data) def encode_int(data: int) -> bytes: - return struct.pack("!I", data) + return struct.pack("!i", data) def encode_long(data: int) -> bytes: - return struct.pack("!L", data) + return struct.pack("!l", data) def encode_bigint(data: int) -> bytes: - return struct.pack("!Q", data) + return struct.pack("!q", data) def encode_str(s: str) -> bytes: try: From 0ca041c0424e104efc59662c35fb61aae5048dd2 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Mon, 13 Nov 2023 17:36:22 -0500 Subject: [PATCH 081/101] sao: fix date formatting in YuiMedalShopData --- titles/sao/handlers/helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/titles/sao/handlers/helpers.py b/titles/sao/handlers/helpers.py index b6426a0..2a29506 100644 --- a/titles/sao/handlers/helpers.py +++ b/titles/sao/handlers/helpers.py @@ -719,8 +719,8 @@ class YuiMedalShopData(BaseHelper): ret += encode_byte(self.target_days) ret += encode_byte(self.target_hour) ret += encode_byte(self.interval_hour) - ret += encode_str(self.sales_end_date) - ret += encode_str(self.sales_end_date) + ret += encode_str(fmt_dt(self.sales_end_date)) + ret += encode_str(fmt_dt(self.sales_end_date)) ret += encode_byte(self.sort) return ret From 0003ca4412033fdeea8ecb95b1d7c46b954b3882 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Mon, 13 Nov 2023 17:39:45 -0500 Subject: [PATCH 082/101] sao: fix bad handlers --- titles/sao/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/titles/sao/base.py b/titles/sao/base.py index 2470e7e..f72f177 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -928,8 +928,8 @@ class SaoBase: def handle_d5fc(self, header: SaoRequestHeader, request: bytes) -> bytes: # master_data/get_m_gasha_medal_shops - pass + return SaoNoopResponse(header.cmd + 1).make() def handle_d604(self, header: SaoRequestHeader, request: bytes) -> bytes: # master_data_2/get_m_res_earn_campaign_shops - pass \ No newline at end of file + return SaoNoopResponse(header.cmd + 1).make() \ No newline at end of file From 1ba14da90c4052c6f656d107972b87d1700a8044 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Tue, 14 Nov 2023 13:20:05 -0500 Subject: [PATCH 083/101] sao: gasha medal shop handler classes --- titles/sao/handlers/base.py | 18 ++++++++++++++++++ titles/sao/handlers/helpers.py | 5 +++++ 2 files changed, 23 insertions(+) diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py index 426ca5f..3b7e17a 100644 --- a/titles/sao/handlers/base.py +++ b/titles/sao/handlers/base.py @@ -3194,3 +3194,21 @@ class GetMYuiMedalShopItemsResponse(SaoBaseResponse): self.header.length = len(ret) return super().make() + ret + +class GetMGashaMedalShopsRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + self.dummy = decode_byte(data, 0) + +class GetMGashaMedalShopsResponse(SaoBaseResponse): + def __init__(self, cmd_id: int) -> None: + super().__init__(cmd_id) + self.result = 1 # byte + self.data_list: List[GashaMedalShop] = [] + + def make(self) -> bytes: + ret = encode_byte(self.result) + ret += encode_arr_cls(self.data_list) + + self.header.length = len(ret) + return super().make() + ret diff --git a/titles/sao/handlers/helpers.py b/titles/sao/handlers/helpers.py index 2a29506..230ab10 100644 --- a/titles/sao/handlers/helpers.py +++ b/titles/sao/handlers/helpers.py @@ -803,3 +803,8 @@ class YuiMedalShopItemData(BaseHelper): ret += encode_int(self.property4_value1) ret += encode_int(self.property4_value2) return ret + +class GashaMedalShop(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + \ No newline at end of file From dffa11f4203f4b5d6bfe96127539abe8bf7e1211 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Tue, 14 Nov 2023 20:51:51 -0500 Subject: [PATCH 084/101] sao: fix get_m_gasha_medal_shops --- titles/sao/base.py | 18 +++++++++-- titles/sao/handlers/helpers.py | 56 ++++++++++++++++++++++++++++++++-- 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/titles/sao/base.py b/titles/sao/base.py index f72f177..8d8cfb0 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -894,7 +894,7 @@ class SaoBase: tmp.sales_end_date = datetime(2100, 1, 1, 0, 0, 0, 0) # always open resp.data_list.append(tmp) - + self.logger.debug(f"Load {len(resp.data_list)} Yui Medal Shops") return resp.make() def handle_d5dc(self, header: SaoRequestHeader, request: bytes) -> bytes: @@ -923,12 +923,24 @@ class SaoBase: tmp.property4_value2 = int(shop['Property4Value2']) resp.data_list.append(tmp) - + + self.logger.debug(f"Load {len(resp.data_list)} Yui Medal Shop Items") return resp.make() def handle_d5fc(self, header: SaoRequestHeader, request: bytes) -> bytes: # master_data/get_m_gasha_medal_shops - return SaoNoopResponse(header.cmd + 1).make() + req = GetMGashaMedalShopsRequest(header.cmd + 1).make() + resp = GetMGashaMedalShopsResponse(header.cmd + 1) + + shops = self.load_data_csv("GashaMedalShops") + for shop in shops: + tmp = GashaMedalShop.from_args(shop['GashaMedalShopId'], shop['Name'], shop['GashaMedalId'], shop['UseGashaMedalNum'], shop['PurchaseLimit']) + tmp.sales_end_date = datetime(2100, 1, 1, 0, 0, 0, 0) # always open + + resp.data_list.append(tmp) + + self.logger.debug(f"Load {len(resp.data_list)} Yui Medal Shop Items") + return resp.make() def handle_d604(self, header: SaoRequestHeader, request: bytes) -> bytes: # master_data_2/get_m_res_earn_campaign_shops diff --git a/titles/sao/handlers/helpers.py b/titles/sao/handlers/helpers.py index 230ab10..74f8926 100644 --- a/titles/sao/handlers/helpers.py +++ b/titles/sao/handlers/helpers.py @@ -673,17 +673,29 @@ class YuiMedalShopData(BaseHelper): self._sz += description[1] self.selling_yui_medal = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF self.selling_col = decode_int(data, offset + self._sz) + self._sz += INT_OFF self.selling_event_item_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF self.selling_event_item_num = decode_int(data, offset + self._sz) + self._sz += INT_OFF self.selling_ticket_num = decode_int(data, offset + self._sz) + self._sz += INT_OFF self.purchase_limit = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF self.pick_up_flag = decode_byte(data, offset + self._sz) + self._sz += BYTE_OFF self.product_category = decode_byte(data, offset + self._sz) + self._sz += BYTE_OFF self.sales_type = decode_byte(data, offset + self._sz) + self._sz += BYTE_OFF self.target_days = decode_byte(data, offset + self._sz) + self._sz += BYTE_OFF self.target_hour = decode_byte(data, offset + self._sz) + self._sz += BYTE_OFF self.interval_hour = decode_byte(data, offset + self._sz) + self._sz += BYTE_OFF sales_start_date = decode_str(data, offset + self._sz) self.sales_start_date = prs_dt(sales_start_date[0]) @@ -719,7 +731,7 @@ class YuiMedalShopData(BaseHelper): ret += encode_byte(self.target_days) ret += encode_byte(self.target_hour) ret += encode_byte(self.interval_hour) - ret += encode_str(fmt_dt(self.sales_end_date)) + ret += encode_str(fmt_dt(self.sales_start_date)) ret += encode_str(fmt_dt(self.sales_end_date)) ret += encode_byte(self.sort) return ret @@ -807,4 +819,44 @@ class YuiMedalShopItemData(BaseHelper): class GashaMedalShop(BaseHelper): def __init__(self, data: bytes, offset: int) -> None: super().__init__(data, offset) - \ No newline at end of file + self.gasha_medal_shop_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + name = decode_str(data, offset + self._sz) + self.name = name[0] + self._sz += name[1] + + self.gasha_medal_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.use_gasha_medal_num = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.purchase_limit = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + + sales_start_date = decode_str(data, offset + self._sz) + self.sales_start_date = prs_dt(sales_start_date[0]) + self._sz += sales_start_date[1] + + sales_end_date = decode_str(data, offset + self._sz) + self.sales_end_date = prs_dt(sales_end_date[0]) + self._sz += sales_end_date[1] + + @classmethod + def from_args(cls, shop_id: int = 0, name: str = "", medal_id: int = 0, medal_num: int = 0, purchase_limit: int = 0) -> "GashaMedalShop": + ret = cls(b"\x00" * 26, 0) + ret.gasha_medal_shop_id = shop_id + ret.name = name + ret.gasha_medal_id = medal_id + ret.use_gasha_medal_num = medal_num + ret.purchase_limit = purchase_limit + return ret + + def make(self) -> bytes: + ret = encode_int(self.gasha_medal_shop_id) + ret += encode_str(self.name) + ret += encode_int(self.gasha_medal_id) + ret += encode_int(self.use_gasha_medal_num) + ret += encode_short(self.purchase_limit) + ret += encode_str(fmt_dt(self.sales_start_date)) + ret += encode_str(fmt_dt(self.sales_end_date)) + return ret From 9c49a4f54aa543d3e0bbe24f2e26a372aaefe34a Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Tue, 14 Nov 2023 21:07:58 -0500 Subject: [PATCH 085/101] sao: fix get_m_res_earn_campaign_shops --- titles/sao/base.py | 22 ++++++++++++-- titles/sao/handlers/base.py | 18 +++++++++++ titles/sao/handlers/helpers.py | 55 ++++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 2 deletions(-) diff --git a/titles/sao/base.py b/titles/sao/base.py index 8d8cfb0..3cec6d2 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -939,9 +939,27 @@ class SaoBase: resp.data_list.append(tmp) - self.logger.debug(f"Load {len(resp.data_list)} Yui Medal Shop Items") + self.logger.debug(f"Load {len(resp.data_list)} Gasha Medal Shops") return resp.make() def handle_d604(self, header: SaoRequestHeader, request: bytes) -> bytes: # master_data_2/get_m_res_earn_campaign_shops - return SaoNoopResponse(header.cmd + 1).make() \ No newline at end of file + req = GetMResEarnCampaignShopsRequest(header, request) + resp = GetMResEarnCampaignShopsResponse(header.cmd + 1) + + shops = self.load_data_csv("ResEarnCampaignShops") + for shop in shops: + tmp = ResEarnCampaignShop.from_args(shop['ResEarnCampaignShopId'], shop['ResEarnCampaignApplicationId'], shop['Name']) + tmp.selling_yui_medal = shop['SellingYuiMedal'] + tmp.selling_col = shop['SellingCol'] + tmp.selling_event_item_id = shop['SellingEventItemId'] + tmp.selling_event_item_num = shop['SellingEventItemNum'] + tmp.purchase_limit = shop['PurchaseLimit'] + tmp.get_application_point = shop['GetApplicationPoint'] + + tmp.sales_end_date = datetime(2100, 1, 1, 0, 0, 0, 0) # always open + + resp.data_list.append(tmp) + + self.logger.debug(f"Load {len(resp.data_list)} Res Earn Campaign Shops") + return resp.make() diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py index 3b7e17a..4178640 100644 --- a/titles/sao/handlers/base.py +++ b/titles/sao/handlers/base.py @@ -3212,3 +3212,21 @@ class GetMGashaMedalShopsResponse(SaoBaseResponse): self.header.length = len(ret) return super().make() + ret + +class GetMResEarnCampaignShopsRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + self.dummy = decode_byte(data, 0) + +class GetMResEarnCampaignShopsResponse(SaoBaseResponse): + def __init__(self, cmd_id: int) -> None: + super().__init__(cmd_id) + self.result = 1 # byte + self.data_list: List[ResEarnCampaignShop] = [] + + def make(self) -> bytes: + ret = encode_byte(self.result) + ret += encode_arr_cls(self.data_list) + + self.header.length = len(ret) + return super().make() + ret diff --git a/titles/sao/handlers/helpers.py b/titles/sao/handlers/helpers.py index 74f8926..6e11146 100644 --- a/titles/sao/handlers/helpers.py +++ b/titles/sao/handlers/helpers.py @@ -816,6 +816,61 @@ class YuiMedalShopItemData(BaseHelper): ret += encode_int(self.property4_value2) return ret +class ResEarnCampaignShop(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + self.res_earn_campaign_shop_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.res_earn_campaign_application_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + name = decode_str(data, offset + self._sz) + self.name = name[0] + self._sz += name[1] + + self.selling_yui_medal = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + self.selling_col = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.selling_event_item_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.selling_event_item_num = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.purchase_limit = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + self.get_application_point = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + + sales_start_date = decode_str(data, offset + self._sz) + self.sales_start_date = prs_dt(sales_start_date[0]) + self._sz += sales_start_date[1] + + sales_end_date = decode_str(data, offset + self._sz) + self.sales_end_date = prs_dt(sales_end_date[0]) + self._sz += sales_end_date[1] + + @classmethod + def from_args(cls, shop_id: int = 0, app_id: int = 0, name: str = "") -> "ResEarnCampaignShop": + ret = cls(b"\x00" * 26, 0) + ret.res_earn_campaign_shop_id = shop_id + ret.res_earn_campaign_application_id = app_id + ret.name = name + return ret + + def make(self) -> bytes: + ret = encode_int(self.res_earn_campaign_shop_id) + ret = encode_int(self.res_earn_campaign_application_id) + ret += encode_str(self.name) + ret += encode_short(self.selling_yui_medal) + ret += encode_int(self.selling_col) + ret += encode_int(self.selling_event_item_id) + ret += encode_int(self.selling_event_item_num) + ret += encode_short(self.purchase_limit) + ret += encode_short(self.get_application_point) + ret += encode_str(fmt_dt(self.sales_start_date)) + ret += encode_str(fmt_dt(self.sales_end_date)) + return ret + class GashaMedalShop(BaseHelper): def __init__(self, data: bytes, offset: int) -> None: super().__init__(data, offset) From 7eb095cdb4e9bfcbad871e64d5fbf45c1fc86040 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Tue, 14 Nov 2023 21:18:22 -0500 Subject: [PATCH 086/101] soa: fix get_m_gasha_medal_shops --- titles/sao/base.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/titles/sao/base.py b/titles/sao/base.py index 3cec6d2..6a1637c 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -891,7 +891,7 @@ class SaoBase: tmp.interval_hour = int(shop['IntervalHour']) tmp.sort = int(shop['Sort']) - tmp.sales_end_date = datetime(2100, 1, 1, 0, 0, 0, 0) # always open + tmp.sales_end_date = datetime(2121, 1, 1, 0, 0, 0, 0) # always open resp.data_list.append(tmp) self.logger.debug(f"Load {len(resp.data_list)} Yui Medal Shops") @@ -929,19 +929,23 @@ class SaoBase: def handle_d5fc(self, header: SaoRequestHeader, request: bytes) -> bytes: # master_data/get_m_gasha_medal_shops - req = GetMGashaMedalShopsRequest(header.cmd + 1).make() + req = GetMGashaMedalShopsRequest(header, request) resp = GetMGashaMedalShopsResponse(header.cmd + 1) shops = self.load_data_csv("GashaMedalShops") for shop in shops: tmp = GashaMedalShop.from_args(shop['GashaMedalShopId'], shop['Name'], shop['GashaMedalId'], shop['UseGashaMedalNum'], shop['PurchaseLimit']) - tmp.sales_end_date = datetime(2100, 1, 1, 0, 0, 0, 0) # always open + tmp.sales_end_date = datetime(2121, 1, 1, 0, 0, 0, 0) # always open resp.data_list.append(tmp) self.logger.debug(f"Load {len(resp.data_list)} Gasha Medal Shops") return resp.make() + def handle_d5fe(self, header: SaoRequestHeader, request: bytes) -> bytes: + # master_data/get_m_gasha_medal_shop_items + return SaoNoopResponse(header.cmd + 1) + def handle_d604(self, header: SaoRequestHeader, request: bytes) -> bytes: # master_data_2/get_m_res_earn_campaign_shops req = GetMResEarnCampaignShopsRequest(header, request) @@ -957,9 +961,13 @@ class SaoBase: tmp.purchase_limit = shop['PurchaseLimit'] tmp.get_application_point = shop['GetApplicationPoint'] - tmp.sales_end_date = datetime(2100, 1, 1, 0, 0, 0, 0) # always open + tmp.sales_end_date = datetime(2121, 1, 1, 0, 0, 0, 0) # always open resp.data_list.append(tmp) self.logger.debug(f"Load {len(resp.data_list)} Res Earn Campaign Shops") return resp.make() + + def handle_d606(self, header: SaoRequestHeader, request: bytes) -> bytes: + # master_data_2/get_m_res_earn_campaign_shop_items + return SaoNoopResponse(header.cmd + 1) \ No newline at end of file From 3d8ffaa098cceea58f6fc37b190fcd30b1b0df8a Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Tue, 14 Nov 2023 21:24:33 -0500 Subject: [PATCH 087/101] sao: fix csc fields not being integers --- titles/sao/base.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/titles/sao/base.py b/titles/sao/base.py index 6a1637c..24a3c2a 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -934,7 +934,7 @@ class SaoBase: shops = self.load_data_csv("GashaMedalShops") for shop in shops: - tmp = GashaMedalShop.from_args(shop['GashaMedalShopId'], shop['Name'], shop['GashaMedalId'], shop['UseGashaMedalNum'], shop['PurchaseLimit']) + tmp = GashaMedalShop.from_args(int(shop['GashaMedalShopId']), shop['Name'], int(shop['GashaMedalId']), int(shop['UseGashaMedalNum']), int(shop['PurchaseLimit'])) tmp.sales_end_date = datetime(2121, 1, 1, 0, 0, 0, 0) # always open resp.data_list.append(tmp) @@ -953,13 +953,13 @@ class SaoBase: shops = self.load_data_csv("ResEarnCampaignShops") for shop in shops: - tmp = ResEarnCampaignShop.from_args(shop['ResEarnCampaignShopId'], shop['ResEarnCampaignApplicationId'], shop['Name']) - tmp.selling_yui_medal = shop['SellingYuiMedal'] - tmp.selling_col = shop['SellingCol'] - tmp.selling_event_item_id = shop['SellingEventItemId'] - tmp.selling_event_item_num = shop['SellingEventItemNum'] - tmp.purchase_limit = shop['PurchaseLimit'] - tmp.get_application_point = shop['GetApplicationPoint'] + tmp = ResEarnCampaignShop.from_args(int(shop['ResEarnCampaignShopId']), int(shop['ResEarnCampaignApplicationId']), shop['Name']) + tmp.selling_yui_medal = int(shop['SellingYuiMedal']) + tmp.selling_col = int(shop['SellingCol']) + tmp.selling_event_item_id = int(shop['SellingEventItemId']) + tmp.selling_event_item_num = int(shop['SellingEventItemNum']) + tmp.purchase_limit = int(shop['PurchaseLimit']) + tmp.get_application_point = int(shop['GetApplicationPoint']) tmp.sales_end_date = datetime(2121, 1, 1, 0, 0, 0, 0) # always open From e0aeb2ac2139e540d7290b376c7d03588c1e75cc Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Tue, 14 Nov 2023 22:08:09 -0500 Subject: [PATCH 088/101] sao: fix temp responses --- titles/sao/base.py | 4 ++-- titles/sao/index.py | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/titles/sao/base.py b/titles/sao/base.py index 24a3c2a..f6bb449 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -944,7 +944,7 @@ class SaoBase: def handle_d5fe(self, header: SaoRequestHeader, request: bytes) -> bytes: # master_data/get_m_gasha_medal_shop_items - return SaoNoopResponse(header.cmd + 1) + return SaoNoopResponse(header.cmd + 1).make() def handle_d604(self, header: SaoRequestHeader, request: bytes) -> bytes: # master_data_2/get_m_res_earn_campaign_shops @@ -970,4 +970,4 @@ class SaoBase: def handle_d606(self, header: SaoRequestHeader, request: bytes) -> bytes: # master_data_2/get_m_res_earn_campaign_shop_items - return SaoNoopResponse(header.cmd + 1) \ No newline at end of file + return SaoNoopResponse(header.cmd + 1).make() \ No newline at end of file diff --git a/titles/sao/index.py b/titles/sao/index.py index be7c3cf..a6ca141 100644 --- a/titles/sao/index.py +++ b/titles/sao/index.py @@ -119,6 +119,12 @@ class SaoServlet(BaseServlet): self.logger.info(f"{endpoint} - {cmd_str} request") self.logger.debug(f"Request: {req_raw.hex()}") resp = handler(req_header, req_data) + + if resp is None: + resp = SaoNoopResponse(req_header.cmd + 1).make() + + elif issubclass(resp, SaoBaseResponse): + resp = resp.make() self.logger.debug(f"Response: {resp.hex()}") From 328f64ad8abb798c743c730e925208529e5cd699 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Tue, 14 Nov 2023 22:11:24 -0500 Subject: [PATCH 089/101] sao: hotfix --- titles/sao/index.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/titles/sao/index.py b/titles/sao/index.py index a6ca141..b70f02a 100644 --- a/titles/sao/index.py +++ b/titles/sao/index.py @@ -123,9 +123,16 @@ class SaoServlet(BaseServlet): if resp is None: resp = SaoNoopResponse(req_header.cmd + 1).make() + if type(resp) == bytes: + pass + elif issubclass(resp, SaoBaseResponse): resp = resp.make() + else: + self.logger.error(f"Unknown response type {type(resp)}") + return b"" + self.logger.debug(f"Response: {resp.hex()}") if self.game_cfg.crypt.enable: From 67c9ac5ef927c7df166eb97348d34cb8c90d638f Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Wed, 15 Nov 2023 00:46:51 -0500 Subject: [PATCH 090/101] sao: noop get_m_res_earn_campaign_shops --- titles/sao/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/titles/sao/base.py b/titles/sao/base.py index f6bb449..09a9f88 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -965,8 +965,8 @@ class SaoBase: resp.data_list.append(tmp) - self.logger.debug(f"Load {len(resp.data_list)} Res Earn Campaign Shops") - return resp.make() + #self.logger.debug(f"Load {len(resp.data_list)} Res Earn Campaign Shops") + return SaoNoopResponse(header.cmd + 1).make() def handle_d606(self, header: SaoRequestHeader, request: bytes) -> bytes: # master_data_2/get_m_res_earn_campaign_shop_items From 04c708be8c9178904f0f002711cf29d07eff4f3b Mon Sep 17 00:00:00 2001 From: phantomlan Date: Wed, 15 Nov 2023 15:09:57 +0100 Subject: [PATCH 091/101] update item.py put_mission_point --- titles/ongeki/schema/item.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/ongeki/schema/item.py b/titles/ongeki/schema/item.py index 2c5b526..1214dac 100644 --- a/titles/ongeki/schema/item.py +++ b/titles/ongeki/schema/item.py @@ -533,8 +533,8 @@ class OngekiItemData(BaseData): return result.fetchall() def put_mission_point(self, aime_id: int, version: int, mission_point_data: Dict) -> Optional[int]: - mission_point_data["user"] = aime_id mission_point_data["version"] = version + mission_point_data["user"] = aime_id sql = insert(mission_point).values(**mission_point_data) conflict = sql.on_duplicate_key_update(**mission_point_data) From dcebc5c21a4290afc221b276208e06eec125801d Mon Sep 17 00:00:00 2001 From: phantomlan Date: Wed, 15 Nov 2023 15:09:57 +0100 Subject: [PATCH 092/101] update item.py put_mission_point --- titles/ongeki/schema/item.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/ongeki/schema/item.py b/titles/ongeki/schema/item.py index 2c5b526..1214dac 100644 --- a/titles/ongeki/schema/item.py +++ b/titles/ongeki/schema/item.py @@ -533,8 +533,8 @@ class OngekiItemData(BaseData): return result.fetchall() def put_mission_point(self, aime_id: int, version: int, mission_point_data: Dict) -> Optional[int]: - mission_point_data["user"] = aime_id mission_point_data["version"] = version + mission_point_data["user"] = aime_id sql = insert(mission_point).values(**mission_point_data) conflict = sql.on_duplicate_key_update(**mission_point_data) From 69cd79003ce7ec43cdb9a82447f7276a9c02cc64 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 17 Nov 2023 10:45:18 -0500 Subject: [PATCH 093/101] wacca: fix exception handling --- titles/wacca/index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/wacca/index.py b/titles/wacca/index.py index d27dc8e..a3a0655 100644 --- a/titles/wacca/index.py +++ b/titles/wacca/index.py @@ -179,4 +179,4 @@ class WaccaServlet: resp = BaseResponse() resp.status = 1 resp.message = "A server error occoured." - return end(resp) + return end(resp.make()) From 9a017f75e96aa9871a9c65629027a12d100f19bc Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 17 Nov 2023 10:57:12 -0500 Subject: [PATCH 094/101] wacca: add traceback printing --- titles/wacca/index.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/titles/wacca/index.py b/titles/wacca/index.py index a3a0655..61c7272 100644 --- a/titles/wacca/index.py +++ b/titles/wacca/index.py @@ -7,6 +7,7 @@ from hashlib import md5 from twisted.web.http import Request from typing import Dict, Tuple, List from os import path +import traceback from core import CoreConfig, Utils from .config import WaccaConfig @@ -173,8 +174,10 @@ class WaccaServlet: self.logger.error( f"{req_json['appVersion']} Error handling method {url_path} -> {e}" ) - if self.core_cfg.server.is_develop: - raise + if self.logger.level == logging.DEBUG: + traceback.print_exception(e, limit=1) + with open("{0}/{1}.log".format(self.core_cfg.server.log_dir, "wacca"), "a") as f: + traceback.print_exception(e, limit=1, file=f) resp = BaseResponse() resp.status = 1 From 05be10dabcee8c720b0a98cf089761dd46ae933d Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 17 Nov 2023 11:37:11 -0500 Subject: [PATCH 095/101] wacca: add request validation, fix HousingStartRequestV2 --- titles/wacca/handlers/housing.py | 2 ++ titles/wacca/index.py | 24 +++++++++++++++++------- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/titles/wacca/handlers/housing.py b/titles/wacca/handlers/housing.py index f2f079e..e11c61a 100644 --- a/titles/wacca/handlers/housing.py +++ b/titles/wacca/handlers/housing.py @@ -32,6 +32,8 @@ class HousingStartRequestV1(BaseRequest): class HousingStartRequestV2(HousingStartRequestV1): def __init__(self, data: Dict) -> None: super(HousingStartRequestV1, self).__init__(data) + self.unknown0: str = self.params[0] + self.errorLog: str = self.params[1] self.creditLog: str = self.params[2] self.info: List[HousingInfo] = [] diff --git a/titles/wacca/index.py b/titles/wacca/index.py index 61c7272..dbe52ca 100644 --- a/titles/wacca/index.py +++ b/titles/wacca/index.py @@ -18,7 +18,7 @@ from .lilyr import WaccaLilyR from .lily import WaccaLily from .s import WaccaS from .base import WaccaBase -from .handlers.base import BaseResponse +from .handlers.base import BaseResponse, BaseRequest from .handlers.helpers import Version @@ -115,10 +115,20 @@ class WaccaServlet: try: req_json = json.loads(request.content.getvalue()) version_full = Version(req_json["appVersion"]) + req = BaseRequest(req_json) - except Exception: + except KeyError as e: self.logger.error( - f"Failed to parse request to {url_path} -> {request.content.getvalue()}" + f"Failed to parse request to {request.content.getvalue()} -> Missing required value {e}" + ) + resp = BaseResponse() + resp.status = 1 + resp.message = "不正なリクエスト エラーです" + return end(resp.make()) + + except Exception as e: + self.logger.error( + f"Failed to parse request to {url_path} -> {request.content.getvalue()} -> {e}" ) resp = BaseResponse() resp.status = 1 @@ -144,7 +154,7 @@ class WaccaServlet: else: self.logger.warning( - f"Unsupported version ({req_json['appVersion']}) request {url_path} - {req_json}" + f"Unsupported version ({req.appVersion}) request {url_path} - {req_json}" ) resp = BaseResponse() resp.status = 1 @@ -152,7 +162,7 @@ class WaccaServlet: return end(resp.make()) self.logger.info( - f"v{req_json['appVersion']} {url_path} request from {client_ip} with chipId {req_json['chipId']}" + f"v{req.appVersion} {url_path} request from {client_ip} with chipId {req.chipId}" ) self.logger.debug(req_json) @@ -167,12 +177,12 @@ class WaccaServlet: handler = getattr(self.versions[internal_ver], func_to_find) resp = handler(req_json) - self.logger.debug(f"{req_json['appVersion']} response {resp}") + self.logger.debug(f"{req.appVersion} response {resp}") return end(resp) except Exception as e: self.logger.error( - f"{req_json['appVersion']} Error handling method {url_path} -> {e}" + f"{req.appVersion} Error handling method {url_path} -> {e}" ) if self.logger.level == logging.DEBUG: traceback.print_exception(e, limit=1) From 0422a2bfd4bdc7007bbd0bdc0bce0a770cd5ccc8 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 19 Nov 2023 01:05:16 -0500 Subject: [PATCH 096/101] wacca: fix lily using wrong handler for hosuing/start --- titles/wacca/lily.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/titles/wacca/lily.py b/titles/wacca/lily.py index 2e70299..96aa91a 100644 --- a/titles/wacca/lily.py +++ b/titles/wacca/lily.py @@ -41,19 +41,6 @@ class WaccaLily(WaccaS): resp = GetNewsResponseV3() return resp.make() - def handle_housing_start_request(self, data: Dict) -> Dict: - req = HousingStartRequestV2(data) - - if req.appVersion.country != "JPN" and req.appVersion.country in [ - region.name for region in WaccaConstants.Region - ]: - region_id = WaccaConstants.Region[req.appVersion.country] - else: - region_id = self.region_id - - resp = HousingStartResponseV1(region_id) - return resp.make() - def handle_user_status_create_request(self, data: Dict) -> Dict: req = UserStatusCreateRequest(data) resp = super().handle_user_status_create_request(data) From e7f35db3a440b263e65c4c4a40bf08c520a3516d Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 19 Nov 2023 01:05:34 -0500 Subject: [PATCH 097/101] fix comparability with python 8.7 - 3.10 --- titles/cxb/index.py | 18 ++++++++++-------- titles/wacca/index.py | 6 ++++-- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/titles/cxb/index.py b/titles/cxb/index.py index c243c15..4a1489c 100644 --- a/titles/cxb/index.py +++ b/titles/cxb/index.py @@ -1,7 +1,6 @@ from twisted.web.http import Request import traceback -from twisted.web import resource, server -from twisted.internet import reactor, endpoints +import sys import yaml import json import re @@ -190,9 +189,10 @@ class CxbServlet(BaseServlet): except Exception as e: self.logger.error(f"Error handling request for file {filetype} - {e}") if self.logger.level == logging.DEBUG: - traceback.print_exception(e, limit=1) + tp, val, tb = sys.exc_info() + traceback.print_exception(tp, val, tb, limit=1) with open("{0}/{1}.log".format(self.core_cfg.server.log_dir, "cxb"), "a") as f: - traceback.print_exception(e, limit=1, file=f) + traceback.print_exception(tp, val, tb, limit=1, file=f) return "" self.logger.debug(f"{version_string} Response {resp}") @@ -218,9 +218,10 @@ class CxbServlet(BaseServlet): except Exception as e: self.logger.error(f"Error handling action {subcmd} request - {e}") if self.logger.level == logging.DEBUG: - traceback.print_exception(e, limit=1) + tp, val, tb = sys.exc_info() + traceback.print_exception(tp, val, tb, limit=1) with open("{0}/{1}.log".format(self.core_cfg.server.log_dir, "cxb"), "a") as f: - traceback.print_exception(e, limit=1, file=f) + traceback.print_exception(tp, val, tb, limit=1, file=f) return "" self.logger.debug(f"Response {resp}") @@ -246,9 +247,10 @@ class CxbServlet(BaseServlet): except Exception as e: self.logger.error(f"Error handling auth {subcmd} request - {e}") if self.logger.level == logging.DEBUG: - traceback.print_exception(e, limit=1) + tp, val, tb = sys.exc_info() + traceback.print_exception(tp, val, tb, limit=1) with open("{0}/{1}.log".format(self.core_cfg.server.log_dir, "cxb"), "a") as f: - traceback.print_exception(e, limit=1, file=f) + traceback.print_exception(tp, val, tb, limit=1, file=f) return "" self.logger.debug(f"Response {resp}") diff --git a/titles/wacca/index.py b/titles/wacca/index.py index dbe52ca..0c9798a 100644 --- a/titles/wacca/index.py +++ b/titles/wacca/index.py @@ -8,6 +8,7 @@ from twisted.web.http import Request from typing import Dict, Tuple, List from os import path import traceback +import sys from core import CoreConfig, Utils from .config import WaccaConfig @@ -185,9 +186,10 @@ class WaccaServlet: f"{req.appVersion} Error handling method {url_path} -> {e}" ) if self.logger.level == logging.DEBUG: - traceback.print_exception(e, limit=1) + tp, val, tb = sys.exc_info() + traceback.print_exception(tp, val, tb, limit=1) with open("{0}/{1}.log".format(self.core_cfg.server.log_dir, "wacca"), "a") as f: - traceback.print_exception(e, limit=1, file=f) + traceback.print_exception(tp, val, tb, limit=1, file=f) resp = BaseResponse() resp.status = 1 From d467070ba73e4f667c09ff26aa2c5d69700e9e04 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 19 Nov 2023 01:18:09 -0500 Subject: [PATCH 098/101] wacca: fix handle_user_status_create_request for lily --- titles/wacca/lily.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/titles/wacca/lily.py b/titles/wacca/lily.py index 96aa91a..937f66c 100644 --- a/titles/wacca/lily.py +++ b/titles/wacca/lily.py @@ -43,13 +43,18 @@ class WaccaLily(WaccaS): def handle_user_status_create_request(self, data: Dict) -> Dict: req = UserStatusCreateRequest(data) - resp = super().handle_user_status_create_request(data) + super().handle_user_status_create_request(data) + + new_user = self.data.profile.get_profile(aime_id=req.aimeId) + + if new_user is None: + return BaseResponse().make() self.data.item.put_item( req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210002 ) # Lily, Added Lily - return resp + return UserStatusCreateResponseV1(new_user['id'], new_user['username']).make() def handle_user_status_get_request(self, data: Dict) -> Dict: req = UserStatusGetRequest(data) From b4a0d331d48ee76b1d56fc405647305dffe2e301 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 19 Nov 2023 11:28:42 -0500 Subject: [PATCH 099/101] wacca: fix housing/start for unregistered serials --- titles/wacca/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/titles/wacca/base.py b/titles/wacca/base.py index 0c4cffc..73cb132 100644 --- a/titles/wacca/base.py +++ b/titles/wacca/base.py @@ -96,6 +96,7 @@ class WaccaBase: def handle_housing_start_request(self, data: Dict) -> Dict: req = HousingStartRequestV1(data) + allnet_region_id = None machine = self.data.arcade.get_machine(req.chipId) if machine is not None: From 0eee2e92a8437ae242892750c929a2c70e9326db Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 19 Nov 2023 11:43:46 -0500 Subject: [PATCH 100/101] wacca: fix bad items being given --- titles/wacca/base.py | 19 +++++-------------- titles/wacca/lily.py | 12 ++++++++++-- titles/wacca/reverse.py | 7 +++++++ 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/titles/wacca/base.py b/titles/wacca/base.py index 73cb132..dd3fe24 100644 --- a/titles/wacca/base.py +++ b/titles/wacca/base.py @@ -236,6 +236,11 @@ class WaccaBase: if profileId is None: return BaseResponse().make() + + if profileId == 0: + # We've already made this profile, just return success + new_user = self.data.profile.get_profile(aime_id=req.aimeId) + profileId = new_user['id'] # Insert starting items self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["title"], 104001) @@ -256,25 +261,11 @@ class WaccaBase: self.data.item.put_item( req.aimeId, WaccaConstants.ITEM_TYPES["note_sound"], 105001 ) - self.data.item.put_item( - req.aimeId, WaccaConstants.ITEM_TYPES["note_sound"], 205005 - ) # Added lily self.data.item.put_item( req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210001 ) - self.data.item.put_item( - req.aimeId, WaccaConstants.ITEM_TYPES["user_plate"], 211001 - ) # Added lily - - self.data.item.put_item( - req.aimeId, WaccaConstants.ITEM_TYPES["touch_effect"], 312000 - ) # Added reverse - self.data.item.put_item( - req.aimeId, WaccaConstants.ITEM_TYPES["touch_effect"], 312001 - ) # Added reverse - return UserStatusCreateResponseV2(profileId, req.username).make() def handle_user_status_getDetail_request(self, data: Dict) -> Dict: diff --git a/titles/wacca/lily.py b/titles/wacca/lily.py index 937f66c..71ff27d 100644 --- a/titles/wacca/lily.py +++ b/titles/wacca/lily.py @@ -43,18 +43,26 @@ class WaccaLily(WaccaS): def handle_user_status_create_request(self, data: Dict) -> Dict: req = UserStatusCreateRequest(data) - super().handle_user_status_create_request(data) + ret = super().handle_user_status_create_request(data) new_user = self.data.profile.get_profile(aime_id=req.aimeId) if new_user is None: return BaseResponse().make() + + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["user_plate"], 211001 + ) # Added lily + + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["note_sound"], 205005 + ) # Added lily self.data.item.put_item( req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210002 ) # Lily, Added Lily - return UserStatusCreateResponseV1(new_user['id'], new_user['username']).make() + return ret def handle_user_status_get_request(self, data: Dict) -> Dict: req = UserStatusGetRequest(data) diff --git a/titles/wacca/reverse.py b/titles/wacca/reverse.py index 557ac03..6206940 100644 --- a/titles/wacca/reverse.py +++ b/titles/wacca/reverse.py @@ -315,5 +315,12 @@ class WaccaReverse(WaccaLilyR): self.data.item.put_item( req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 310002 ) # Added reverse + + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["touch_effect"], 312000 + ) # Added reverse + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["touch_effect"], 312001 + ) # Added reverse return resp From 8bd361d3e37e21ea77cac86e9c996ddb72bcec4f Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 19 Nov 2023 12:15:57 -0500 Subject: [PATCH 101/101] wacca: fix some logging --- titles/wacca/index.py | 4 ++-- titles/wacca/lily.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/titles/wacca/index.py b/titles/wacca/index.py index 0c9798a..7277fd8 100644 --- a/titles/wacca/index.py +++ b/titles/wacca/index.py @@ -187,9 +187,9 @@ class WaccaServlet: ) if self.logger.level == logging.DEBUG: tp, val, tb = sys.exc_info() - traceback.print_exception(tp, val, tb, limit=1) + traceback.print_exception(tp, val, tb, limit=3) with open("{0}/{1}.log".format(self.core_cfg.server.log_dir, "wacca"), "a") as f: - traceback.print_exception(tp, val, tb, limit=1, file=f) + traceback.print_exception(tp, val, tb, limit=3, file=f) resp = BaseResponse() resp.status = 1 diff --git a/titles/wacca/lily.py b/titles/wacca/lily.py index 71ff27d..33928ec 100644 --- a/titles/wacca/lily.py +++ b/titles/wacca/lily.py @@ -426,7 +426,7 @@ class WaccaLily(WaccaS): except Exception: self.logger.error( - f"{__name__} Failed to load item {item['item_id']} for user {profile['user']}" + f"Failed to load item {item['item_id']} for user {profile['user']}" ) resp.seasonInfo.level = profile["xp"]