From e0265485ffbbdcdce7d83ec93891a64e9f95f977 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Sun, 1 Oct 2023 03:54:23 +0200 Subject: [PATCH] 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 zcmeIb-Etg9mUVfrr-0#h`XUqm;NABeZEZ9(#p#v}r79y?J*JWA-6z3Pkr`)4dw}2$ zAY-4Ui?%ia1tk{zEO%rsc>eLz+2Nn_PoEz?|Kro!cYpi&{rQ`-^WFKs{qx`d{L81m ze|Y%)?dM;9egE<%m3{0OaAfo z|NHAd@cW1NZy!JN%l~}$&#(XZp~rvD|Mj*1`8W<=XfL1e?r*>T`u?}a|91T!|Mvd> zef#x)xcmCw{qx(G7(bZHZ=d-359aEW9?bP>4`%=UKc?H&9?Ub=%>5}nn2YPvdN?;a zz5i6l*D2LcSZ};9Zq{4I&saIv`-@XrJJ_ zx3_gGe${>a_O|kV<%e@pgTCKgoYW4wzqmW8)w938yg8-ey13l^kb5fJA2svM_4VaR zEuhB-bW-P@!vnfHsgZwtKtHs*uds%$PHM;=37y)azB;Kr{^t7j{=^pb?fr=@>f8I1 zIGvwuEJ&(TXLFDnLc6o9u`|{Kb zd3kwK2kb9DwI4dJ)kXNDzZ89WL??A#J3ON6yHi_7*LOekQ>!vF{M4@RPHpeMyQ+)x zwGDZ9^+VHNznvfdsa@<&>q(=-&cEN)#p(O0JtIIlJfh3%6Fd1I9?=i2qYButj_z(w z%#fFtw=ZJIFG?_-ntX=0@BeQe{e z_T7(dI9cEQ*oI&2dmo#;Uh+fS#$WBbAKP%UzL%TCO_T5RyC2(dvcCJV4ZqrVKepjy zefMJg%(3 z$Di}=pI`U!v!3v)|MCCw`@Z_cs$czb)vtcF>Q}#B^{d~k`qgh&{pxqCe)aoje)W3T zIi885X?lvXj{m3s8mA|I`R7jS6Tkd($MuO{{<-t|#4mp|uwQ=2|9ZCc;kVx&{_)$p z!ygy_`tViu7iBD2r59{orD{_yeP z*ZtwYQ~&q%b1ojud?&9^)K9y7{IuQTr(Hem)2{#ew66@F{7269+xcs!{DPzY@ap%_ z3`hM{qB)Lw(ru2T9DukB;fskOw}U&I>-B^1!DP9v#!Mkv}@7V?!SJ zbUH8ibjkytPIz=o$436>n2rs3;M3{6;L|A&d^+LLF&!KEqhmTY`E(C{ba|yxW+FL?!xVDxM9JX{Ug$FX796fC5 zSjzf9r_=R;PbWNV=~&3T*wV2S9-L06^MX&OJn-p+hb4b+Z9SiY?EgehYflsINf={PB@acqyEgcK-hb4b+Z9SfNkTRN7)gVX7BUhwIZ2R@zf zu%%-m{;;KEDLnA$bYAf3lm|YY@UW$0A^xzXV<|lF>2zN3>68aPo$#=wV10??=>!KVJ;q@x#4_Ar3&cWjpwh{(pwbBrRC5FDSPBn(I-M7MI^}^+Cp;Ek zfqrT<7UB6R`}d+PRbd$;tJPNf_h(Qw5M z88lp5O9qX)Zk~I;S_Tc*wvs`^R*i+=z@x`|tgRUfSsj>kvN}-dF%DZWmN6{0UMvI$ zXVS^ApwbBrRCP=(qkO9I4r{*wl*vT2P&Nm3o4!9K&8hxY+cxP+;RH? zH;UaazMOX|S=fk%D`&`{;o4R*Xt;Kj3>vOYC4+`73k$)4M~{yjwk#}UbzsuT>OiH( zI2O;5PyIuMjA5~5VIep;lTL;Ol}>P=(qkO9EG)wvwk#|J2P&Nm3o4!9K&8hxY*|=_ zJ8W542o6*_85UGJ!GTJTaoDo340qVFun-)mbTTZcbb$@Xt;8Q3>vO&C4+`*SIMAJr|7wN zr)AKvWnm#W@aXX#YsIX`sW3a0E^LIu z)iZ?PaBVChI9wY`2oBfA5`x3lhNbX8rjz%tw$))N>jRxm*9ShG@UR79A@gD@#8P;0 zI-Sl7KArNwrxPBwN-V@5woELA2R@z73qGClz^4-)wp1*{AGTI3g$F*J&I>-B^1!DP z9=2XA#2>a`EQJR?oz4qBo$|n^6CSo`EW{tSYAl5ZKAp}BKArNwrxPBwa%@BXkfz}{ zrCSQVp8WS6i_fH-N;)>g;mRHEGH|$dmk=DT%_RhfYi|j`A!2#-B^1!DP9=3KY#2>bHEQJR?oz4qBo$|n^6CSp9EW{tS zb}WSlKAp}BKArNwrxPBwc5Flb*uTKFWA}GU5!%V5V4b+Z9SiY?EgehYflsINf={PB@acqyEgcK-hb2zN3 z>68aPo$#=wV2zN3>68aPo$#=wW83k^#dV#DHQKS; z&9q}<9A7)X3J=%*lET9lki`JOsArQSh%F(DSs^&JtPrF+1!Akn zQbxv>k;MSvoLWW(sTLqebqd6mlBGDr){?~lL8@hBkZJ*fRHr~}Jz0uFY(ZHJ5Tsg0 z2B{VxNOcOt7L}zq#8#EX070r{WRPkBf>ftKY-L%BLu_eT3=pJRMh2-CAV_rz#Fm$B z$sxBd@UE6On|W8ui|Y{&R}7KD!?nev@Nn%gDLh;oObQQMUKRrcqfU^X* zT8UuRLd2Gw#dyTloaGR~taWNIYbAnN3lUp)7UL0Hc$Py1v(~A>td$66EkqVy%-9=J*5rIZJVfEjfz;f>g`MAk_i{sZN2|a^^<^6x z8QDnI$jkN@i&vMOOvyY1;;JEHfVfWXF+g0KObig$CKCh1)|llGL93;7_uX)ovh(f{ zYh5FlwGgp|W-(^9m1a3aII-5L!K{@CW-UZ)wONcuY`Iwu5zJbr2D4Ton6(hGC1)`n zu{CEoL@;Ze8q8XWVAev!)}6(8#1@|A5W%c{d@d5Qel?^=}d>V&O6i!rM$ zKg%J4S*u4dYawDQ&^9$S`xm(3Y`0&w8v(Jk5-C7zNB9&VwtXfAh;5rm0kWvV zr~dUwj8*q&wH!r!30ln5AlAA@Fl!;=OVCoL#+RVQ5aGmHrv|fDBAB%h@g-;}9`Pk; zF+?zHof^zqiD1@3#FwC@c*K{W#Sp=)b!sqcC4yNC5nqCq;t^kh7DEKH)~Ug)l?Y}n zM0^Qaibs42S_~1)TBioHRw9_S5b-5wTk=SK`G1XD(C!vr96X%@ZN#H~?78)K3J=#d zlfuKb$)xabZ80f4Z1q_T5R5v#h}G7f#jFsVT2=^BodQ`rTRt`EO&J+mcNPPLb7~nG zq*{O=)hQ5LbC%)|TXGfy1gVyhL8=7^Qk?>^ z5S&_82vVH_vE^kcBV)_UVt{Z?EhB?e3lO9_1!Bv~QXFE-%VK~a)iN?jwE#h?Qy{jy zEX5(VyetL?QY|BcR0|NKIt60O%TgR-%gbVbAk{K5NVNb#s#74gye!2bw!AC`2vRL0 zgH#I;q&fv+%geUpkn3`<*cy4+{&w-1mD9PtYy`wrL&N}aZ89-HT$@Y`5Z5LX1H{&t zDPyG|k7?0SRvm7FrwN4FYtwbvAQjeguk?ypAq_X0@d|0U z&y0aI+-Al=IwoifM9^wEigZlS7S;%2t!o6c79t%JwDHt*OwbmHaAK`fgIOyP%vy+a zOwh(2>6oA`5W%c{fZjtSb>BOMd81tOTWP7P+QL@;Y1(lJ3Bd!%E6wm<~4 z)~Ug)l?Y}nL^>vDV~=!9&=!bb);cwqwGzRsg-Fi??eZmVLAzXhrsY&_LA&0MfY@4z z6d<;ZCIyIXpGg5?+h$UL_!6`jB51W7MSKZb%o;(gb&X)wLd2J#rA&=4L5m^6iM37* zX01dpYa!xG&{90&OVDD7VAeV{n6(nYtcA$p6QfVQ1uexRz633X2xhHQgIOyP%vy-} z60{VL_!6`jBAB&K4Q8!GFl!;=OVCn0;!DtCh+x(_HJG&$!K{UdFG1UqN2)4TxdrWF zw=1Pvy+7=960{KzTPu+Qq{6B{)+axKmIB1K&!hmcZ8IrAdvEWQl`e2pv4g3#9F5YvsNOQwGiL<$hwMw0@>w$G#hv28OcKzs>W3=y{d@UxJq65nqB9Lj<$dsllw32xcurdF=Tk9S;Kdls`oaB5j0NOcOt7M`VyjIBG10m3=8j0{pOK#=Mbh^;wGafmHBivfaE z%g7+r0tBf}f!K1h6o=Sqvlt*qwTui>EkKa!6o{=fOL2%TG>ZX(RLjU9)dB>mPJ!4W zvlNHe8nYN6NVSX%QY}D`>J*5rFWZhoZg1)$IDWBv=~}WUzlGxEX8N)*5LXScPXXfE zWMY80HklY8)i2LY?qY!08nYZCXtk8?w#qDLjUd*#Mlfq3Vhhb;rp8v9X*T8UuRLd2Gw#dyTloaGR~taWNIYbAnN3lUp)7UL0H zc$Py1v(~A>td$66EktbbS&T<)^;r%P%vz@gvsNOQwGgotXq)oL{foTG=HfFgm*;QJ z_PfPW?doZtcKz3<{dD%%KYxDvF8}!YFCRa4{^|4YAKt%x_we!UFFzmtSGPAKAhuRw z7Xrk#(WC&e?K3GrY}-r<5MP28Lj-)0f99Lw5nqB9Lj<$dsllw32xcur zdYm-Ug;o4$Sc-ZQ*7$6vRdJ(IwJ&Rc(IJK-0q&fv+ z3(rzU#@3z10O6ckMh2-CAV_rz#MYdpIK-Bm#Q;I7Wn_?Q0fJPgKy0~LibHI*Squ=Q zT1Ez`79dD<3dB~Lr8vYEn#BM?s%2!5Y5{^&r$B6xS&Bn!jadv3q*_J>sTLqebqYk% zqUXLL+?E`&|91D+mm9y<$jkP(i?0EmPG2?x;;JEHfVei97$B}qCI*OWlZgRhYs_+p zpw&{k+bXl1HG){{8o{iE$l@XM$+tzsoL4bxT_c=WD-q0EiD1@3#8#WdoJVZASq>4* zTBioHRw9_S5V0j^F&?osXE{VLYn>X*T8UuRLd4dc#dyRPp5+k1taWNIYbAnN3lUp< z7UL0HeU?K6v(~A>td$66EktYu+NL~GU!+rk>qyX_HXfZ_4YB`Gd6112#f>`Ss!K{UdFF{M08rx-OF$dNQPONomFl!}(Sql+g zf|g=dUxF4x1hdwu!K{@CW-UZ~30jIrd{d@UxKzJk6gaQEoi&lcHU)k1jN=#qyVvP zG$}xA`%DTD+cuK|#FwDO5J9WuDB?@dV%7*^t!o6c79zd`EoEwa30e#hPONomFl!}( zSql+gf|lYDUxF4x1hdwu!K{@CW-UZ~30jIrd{d@UxKzBkL-7HEx;<*pj~dJL0ceV zt0iKH*!G$jBDTFIhKOyii6P<((Q=UB*27z2pC9kPz7#EIm0(v{CFr#x@zrQCgX7E5 za*%L#mBB%;f&{%*B)%jq#wETcEe8pDmBB%;f&{%*B)%>!#wETmEe8pDmBB%;f&{%* zB)&K;#wETwEe8pDmBB%;f&{%*B)&o|#wET)Ee8pDmBB%;f&{%*B)&{-Q!crFfveK) z7ORZQ^EYSv<-G6eX`goe*Qfn-_!)os=g)87N=rKYjlF!~3`I9zMSP<>xQ| ztBVm3S4j~A#I@PP0C8C;_x{+M9}J5eOk^ML9BI+VAev! z7N^Bbjjc_~A;O8ZP7P+QL@;Y1V(ZdkJYvhza)@BoIyIQJ62Yv6h%HHr@rbQR%OQeU z>(pS@N(8eOBDNYW#v`^EEr$qZty6YOswN4FYtwb{fZ#ZzXx&DWNo#dt(+hHigAyTGi~ zBbc=iu_b6R9>dM0Q?BAB&K4Q8!GFl!;w zGeH}9q-TOQB!XG%)L_<11hW<*JrlH%M|vh`Ln4^9P7P+QL@;Y1(lbG;%V3QJZBy66 zjexjXi5MWRjV1<&YoCb$Qpf4Jw~oXBu_b6ZM9^wEir5mgoHc@2>l(qVg@`Rdib%CALB>2nl+X z!9lNr1ie-ywn}YVF4@1pn|0<8>6ucD)=1CoW<;dtZZjg%bF&!{X_~1Gk>J*PBx#zd z4OamL9c=Yy;dYm zGqsURnr3Q4BdqS6~jeyjT)jQ$Z0dZ|MF)PHi z)x-dCZ8R}JY=v455wyD2HEKC)1hLjNf>{d@Tbvd%HMTY_hX^OuIyIQJ62Yv6h^(pS@N(8eOBDN$g#v`^OEr$qZty6YOswN4FY ztwb`Ss!K{UdEkTQ!8e4*vLxdA+of^zqiD1@3 z#Fn7Nc*K^VX*T8UuRLd2G!ZOS866>HprcDMLM%L(0rHUeU6 zB~pM?SkJu{E@p+;_L-CwV%ug?fcO%$7$Ru397TKyTFe?jtaXiG)vEWQas{I&|-*S);cwqwGzRsg@`Xf z+mc7FU*HzB`^D_ygq{o-0dchwF+f}!O$-p%J`)4Pwavr;S#;r3pLEV4f>z5>#Fn7t ztP#Xo*9c}UL~IFK%+%Ntv>YOwSnJea)=C7k79zF;Eyg3Z1TBXMX020$St}9DT8Jz@ zRow2`09%3R_yL3)B+S`HCTtaWNIYbAnN3lUp_ z7UL0Hf|f%Bv(~A>td$66EktYyT8u|*30e*j%vz@gvsNOQwGgo-XfYnKC1^QBFl(I} z%vy-#Twm6~SR+BZ*zGpcppA*RVu>6g zuB|4ANS&zXR%kgyTw6^J5nG2Agao&ql@p1rLx*lM&OB%EDk zaL}tDL9Z2wtw_spi7iPBLV{jpaL}tDL9Z2wElbOBiLFZuLV{jpaL}tDL9Z2wtxd~u zi7ieGLV{jpaL}tDL9Z2wEl|sGiLFozLV{jpaL}tDL9Z2wty0^POK$IL9I!^Fw%;x8 zT_R`k(q{Q8_NLWoki4(5nm%dKDz-wIZ=) zY8!IN#qO#ugmp5t&D^H8K*X0)q!96KHz`DXyG;s_>X_%pohd|&nOY1I+**$$#!M|{ zm0(v{CFr#xF=lEhgJaCpVvullmBB%;f&{%*B*siF#U;i}Ed~jCmBB%;f&{%*B*siF z#U;i}Ed~jCmBB%;f&{%*B*siF#U;i}Ed~jCmBB%;f&{%*B*siF#U;i}Ed~jCmBB%; zf&{%*B*si_OD?(px1SCqzW#GgrZytdGo=_2>ABsEi1gfTMnrmUHX|ZUGqoWS+RgM(fL33{zanr3Pvmo&}PhDgw>3=Vn~ zB!a%OLzsSSy=Oeuy$T5dN(A}x2DA(57w&5%gXOl^z=x7H&` z&rEH+O0cV}67*V;^vu+TgVQrp8zbTDDuaVw1qphsNP1>!Lznc-)W%5As|*f$6(s1j zBI%i_4PDYRQyU{euQE93Rgj?9ibSTW#hZJ`cYXXqU(ZZ!j0C;P;GkDQf?g|F%tADgM(fL33{zanr3QyZd3d4{_VSmk8gkZ`TWh<#qN4Uq-RPoBGPla84>BZ z+l+|x+-ycfnr3Q4B)GL6Nt$MA!&QP^WtE`Uilk|#HX59!nc5HuXIB{<^eRZuYemvD zQyaOYX{I(rf?j2C(5oOpuN6ttOl{mL9c=Yy;dah@_CO;ZRC=snc5Hu zdX>RJuYv@y$TZaT9Gu()M^}X{6`kMlCbqS&yR@Ik3pp8 zc2n2E@znI(ZAL_TZZ;z#O*6G2lKMg1+B8$E3*lI?(tUUIhtytw@??Y9p64&D4fS(5nm%dKDz-wIXSnsf}FHG*cTQL9a46=v9!Q*NUWR zrZ#d((@brM1ii}OpjSbHUMrHOncB!DO*6G267(vAgI)y*daX#BW@?uuMLT{?xM!v| zB2vxUBc&J->ABsEi1gfTMnrmUHX|ZUGqoWS+**$$O*6IOD#5O@O3-UX(lk>W4NlWc zZHR=ks|*f$6(s1jB59hbjac@J7d-c0Mo|>M!&4@_P&1OWTX{I(rf?Mm6q-mx$TqW35Rtb8oNSbD9qrqvK zsST0T4{~ABsEh}4hu=)~)Hee98* zo6U$w(@brM1h>{BNz+VixJv2=vFjea*1>6-snyvy-h-QFYC|NPU1e}8FZ6nWUh67p znyJ;^Ol^n+y~^NJUg-4_z1CIIG*hdyaje&-nc5HudX>ScywK|vdabLZX{J_Z z<5;gvGqoWS^eTf>d7;;9^jcR*(@d?-#<5I~33{zanr3RF!D*VQ z4UuqmmBB%;f&{%*Buz85kxQCpYC|OGRR#yW3KH~Mku=TJMlNZZsSS~!R~a1iDoD_4 zMbb1=8@Z%urZz-^US)95s~|zI6-m=fZRC=snc5HudX>RJuYv@Wt`h7jtEBQmuT3+x zx)6?coTi!D5D913>XHleDoE-QLa$9TwYm_F_1ZL38zMok)g_ncRglyrgkGCwYIPwT z>$Pd7HbjD6t4prXt01XM2)#DV)apVw)@##DZHNTDR+n6(S3y#j5PEHzsnvyWtkI~33{zae3@Fz;P^7N93-4wWpL1|AVIGci7!)&afvTe%Rz!(WpL1| zAVIGci7!)&afvTe%Rz!(WpL1|AVIGc$>M8K`@^e8OD{xSJifvd;}Tz{mV*Sn%HW_^ zL4sZ@5?`hk;}Tz{mV*Sn%HW_^L4sZ@5?`jaDVJP-3%CwMrZ1B(oz#9G$~W-UZ)aazpO*xIxl zBAi(3)L_<11hW<*i@(3OdnUk^rNx-_60_E+!K{@CW-UZ)Nm`6YY(-iQ5zJbr2D4To zn6(hG)o3vuvBhXPL@;Ze8q8XWVAev!7NW&?#MYtZ5W%c{d@TZ6V8kK9~e z%X(YW|$5y5#G2s-evx8%m362#ewn8n)C$>f{i3yI?*}<{O1jh;!TdNl2 z6I-p8#01Cc?BG~sf@6h=ty&B6iLF~pVuE9Jc5tjR!Lh=`)~^Nm#8$8+F~PAqJ2+OE z;8;YliL@#PAv#Y)6!zJN}6suBP31toDq_yTh0hc*OYC@1i{MDq-)AHTqh`2 z*9ndlCS6mu(d=|h*@jFw#p>+fSY?7^g-O?xZRC@#Dcg_6I;p_g?cHWrAaci7jOd@`){FOJag!b#`#9 zGQqLJ#FnxJ`NWp8B{9LVIy*R4nc!GqVoTXJ=9BxExRvdCk?twI!RQEytF+kXAaQLv zK}cMCP7o5;mJ@`;ma-)=L9lW(v88ND>jcH>I>E8R#FnxJ&5kW)OJc$)R%ZvtDia(l zOl&DzkWXwWTM`o-tFwb+l?jd&CbpC<$S1axEr|(^)!D(Z$^^#>6I;p_g?cHWrAaci7jOd@`){FOJag!b#`#9GQqLJWHGjS>a{`J@`*e}vc|1!_lq0Y36*R^ zBCgOPhlp#-$syv}aB_&awwoLxwu&tX32r?cO3sh>Ut7Z#v`Vn6tP=EEk=O#ZoWZg6 zYe7gjyUO68S3!baD-v6~mg5pzx)y{4y~^OAS3!baD-wAI^0_zI=eWdHtpy=LuQE93 zRgj?9io{l|<+#KatOX%KuQE93Rgj?9ie&M%{-=f%IWDoaYC%ZQs|*f$6(s1jBC&O9 z+j7bNMQ&VM+`CSxQyU?1wG{j77$mL@CkTma!wEv-+Hiu9*jlwDCJ6R$Fj=WqOIjx= zR@VuR6(+V|EogRZ#aa>*PO&;WI98e9SYcwT)`EOu%hr;Z;8>j<9IH%ltT3^qYe7D- zwQEUCaIDS_j#VZ&R+!lOwIH9^0=6V3I96u|$0`#XD@+z&^?qu^7UUCK#g@bb$Lj3h zSY?7^g^8_X+n7&kI&ggB%dTW@&;854+q|AT9McSww%GS^=m;$IJ?T=pjSbHUMmt?yO!e;Te=p6 z1ii}OpjSbHUMmt?wwB`(TeTL11ii}OpjSbHUMmt?v6kZ!Td)>{1ii}OpjSbHUMmt? ztd`>vTdNj?1ii}OpjSbHUMmt?r?xGZT$U7VjZE!+@nyqP%G5?gTq#8k5!ZH;L&UY) z@k^mqWz0+vE^&Z8kYXY?)dR65LvkB(_W~Xq8}BStaPTBC%y^IfG-%)Pj(3 zc9p?FuYv@RJuYv@RJuYv@le69 zZGX4h(z||+khofkASA90CkTma!wEv-+Hiu9*jlwDCJ6R$Fj?unKaUgYj^taU)`)An$yp<|iY*8UZao}I&X4zB z-;HZADzUvWXwcxG*E%@1fGua0*!r~~B%EDka5%gQ67*V;*xI!mm)O#^ASCEj1_!+g z67*V;EdH|gTpJN_r4%_tT-!|!5!Y^$ zL&UY&KhSpr4%_tT-!|!5!Y^$L!?gBbMKeSA!5taf{@_WdL*%BYC)?6yUHp-uN8?cQ_C3~ zTc#F-gtMy*4tfwIn7u zR%ZvtDia(lOl;{|kWXywS`rf+tFwb+l?jd&CboVp$S1adEr|(^)!D(Z$^^#>lf@8V zfBxodSFX;fuP9#}J}1a0wu&u@369m-!LiB&#|jf$$+j_{T$gR*_%-yUE#;}(&fTVN zTpJN_g%&wPTw6{K5!Z&3L&UY+)}wcPQwI$lVGVs*CGYE-m!wEuSYt@pNAlSpfWTjdyX`P^0 zT_-qJnAn1~pxLn%Ye`Hv#p>+fSY?7^g^8_N3-XCATT5bsV|8|LtTMr|!o-%Y1^L9* zt|c+Su{t|AR+->fVPfmof_!2N*piswSe+dlt4wgLFtJ5!K|ZlnY)MRTtj-RORVFxA znAl3TjrrvE1#Vp1?-r|#Q%c!JNNlA=4iej@lY_*z=j0%6JN@f;}c)X7Q_U{>g?cHWrAaci7#c#@rf^G z3u1y}b#`#9GQqLJ#Fw&d%O`aytZ^&b-Qo+1r}PT?h=?n+$RXm|a&m~cHk=$Hb)ueo zNnH++#iZz|6L>=~@sH^eTgcUIhtytw?OyT8>L>)mjh|^eTgcUIhtytw?OeT8>L>!CDX! z^eTgcUIhtytw?OKT8>L>ty&Ng^eTgcUIhtytw?O0+O}MB{{lCzUEOc#y+cPtTq#8k z5!ZH;L&UY)95 z)45G;OvIH^=znFaL|ofV4iVRGlS9O{+2jzhWoki4aBDr1*fO=CRf1h*m7v#(#FnY$ z42~^R3qr!#RR#yW3KH~Mk=Qb|9GBQKwIC$uRR#yW3KH~Mk=Qb|9GBQKwIC$uRR#yW z3KH~Mk=Qb|9GBQKwIC$uRR#yW3KH~Mk=Qb|9GBQKwIC$uRR#yW3KH~Mk=Qb|ZMkIs z0=KE{Zx^3=Ii*f*gv8ZS1R-&4I6+8U8%_`s*M<{>#MY`MF+s40gUL$o{h72*P^_*K z94kz0!CKJl*ow6zCY)k*c5tjR!Lh=`R;>m3#FnikF~PAqJ2+OE;8 zfVbU^XTkuKClx>L#j@8-0vC0I;3X_&8+k#J8rff@0aIDS_j#VZ&R+zL**%o}# zGG$w0f@5`daI7-HvBIQl%69(}x3XO>J`-~?x3V1+J3?YBEpm|9Hl5c&a<%*EQ3j9y z{P}Gb{`mSYA3t#Z>GSU&-oJhK@bT?0KcBxjyIEKzwms){knC1KV%u_F2gw?%#Fw(Y z4wE&O$$sO4U1gnY+_0;xlZ`8Om36Xl$F8wXd@0*&IQIVL;_ErYb~y2sY&l3aZP;~I z$)*Ln4w8-gb&Wm8m$AKuO!m7q_8ecv_Bu>fStpw|>`Lnd#Xdf_a{R}$mz!4XD!b0& zQ>RbOq^XV~jaD|WwI#V4CS#ICbWdg?cHWrAaci7#Qx z@rf^C3u1y}b#`#9GQqLJ#22yU_{0~n1u?;~Iy*R4nc!GqA}{>#de@mRW6N>uHICKU z!LiB&#|jf)#+Ks~U&a>11jp*^;8~Q)t%pO&r&QgM(fL33{zaZ0%Z(OKj;{5EAq%gM(fL33{zaY}s0lOKjCz5EAq% zgM(fL33{zaY{goROKibf5EAq%gM(fL33{zaY_VF7OKh!L5EAq%gM(fL33{zaY@OPc zTynXqx>zGq+h1;|QyU|3wG=@}>I!;py7x**p7*Zi<5l9?a9#StlD;>?-SItxf0U1^=5*vI3`@iUH>n^x>9yN)ed3$lr=T6-lXYwS8# zn>Oq^qk~|pV6QeU*!^yevx%)+3$lqVTuWlIX~V8_hP~djVpsCX#vQxH>BJVVy^>GX z*mrF8+AA?xWu0u=v8$Y7Z#J#ib&zb@u&W@kBj{`RWam)F9MpG;bMm(Qoa zVd&y>FsJi|p_jW65?fty_(iOcui4jDkk~ey93-|4CkKgb!^uJ7%h!UKAXwRkZI`V* z{n>FiOgt`i(9OnmuT&g}U5wIC*(Vs&f zVY2v(Tf^sKeGyxZV|@`@5EC4$KEbiV#Fw$<%#JT(3u3}KR%ZvtDia(lOnezzj!%3U zTM!c*tFwb+l?jd&CcctwTRy4du|~>vd$*-m&__gEp+yc6*Orq*q^_TuC_eQ`#~dQA z?Iwqatzrv8f?E%VlF7XUwuUWem0(v{CFr#xu?1{7gJbL0f{<`_mBB%;f&{%*B(`=f z$0fFOEeHvEmBB%;f&{%*B(`iV$0fFEEeHvEmBB%;f&{%*B(`EL$0fF4EeHvEmBB%; zf&{%*B(_*B$0fE_EeHvEmBB%;f&{%*B(_d%TQ0dRSFo&+sqJqUUpG9Z>%~S$TrEWq z64!KdtrHZh>jcLNlf{GQQ={>q*|8OCNlZA! z>g?cHWrAaciLF`-@`){5OJag!b#`#9GQqLJ#FnlF`NY<)B{9LVIy*R4nc!GqV(Zs} zd}0gOl9=FFogEyjOmM6)u|;e_KCxA7Nlb98&JK=MCOB4@*h;pI`J^s|RZ_O=O^g=n%FL0OPU=NtLp^E3KL(-mg87o z$`-_gQ>@Moj#VZ&R+#uwwj7`MQnnx_I96u|$0`#XD@=STTaHhBDO(T|9ILZ~W0eVx z6(+uvEypLmlr4w}j@8-0vC0I;3KL(-mg5s&$`-@~$Lj3hSY?7^g^4d^+m=u6U*cA_ z-EK?o9y&r|D=l)6*fyOUB(^;#2Z?RV$wA^v*@BoLSUH;bQnsLVf?{=@;8*}<{O1jh;!U&@x_6JN>}#01Cc?BG~s zf@6h=FJ;T|i7#afVuE9Jc5tjR!Lh=`m$K#f#Fw%KF~PAqJ2+OE;8TX4$U$P;baIf`_M99fwk;@Moj#VZ&R+#uwwj7`MQnnx_I96u|$0`#XD@=STTaHhBDO(T|9ILZ~ zW0eVx6(+uvEypLmlr4w}j@8-0vC0I;3KQuKp8f7jIX>~FY(Y$Ltj-RORVFxAnD|n* zZTV#X0=Kf=Ek+xc=WovT%MIYw(?0F`uTT5w@u&Rw&!6AE$v?jS%f}C#fBO9Uhxc#a zJ$!uo%g=}Z)y>WI&4`FAw8$ai+H!J;xHg;|BChQwhsdH3_Xim*-DG)j_?#dlxb<;E zI!?)L4O`GE!LG7O&}&6v3)pf7$JVa}A>r&QgM(fL33{zaZ0%Z(OKj;{5EAq%gM(fL z33{zaY}s0lOKjCz5EAq%gM(fL33{zaY{goROKibf5EAq%gM(fL33{zaY_VF7OKh!L z5EAq%gM(fL33{zaY@OP+TvFqJoI*F&Y6!(iS8=g|8HX>3#_S{}Phlp#t$syv} zZE}dXHk%wGwoEMu32v=N5?iJgv`Vn6tP=EEku07!8=hRztE>{vu7U);3KH~Mk=Qb| zoO6jSQwu_ZUS)95s~|zI6^SiV%W;VmL9c=Yy;dYmGqsURnr3Q4B!I?a)`J#n;asxOf3irZmmZWTc#GYO0cV}67*V;*fO=8!Lem(K}a~e z%HW_^L4sZ@5?iL0;}Tn@7K8-7%HW_^L4sZ@5?iL0;}Tn@7K8-7%HW_^L4sZ@5?iL0 z;}Tn@7K8-7%HW_^L4sZ@5?iL0;}Tn@7K8-7%HW_^L4sZ@5?iLWC70ao>uju%sqHUz zOBb`9&bxk&k+@olASA90CkRO$s^_+QK}cL1P7o4XtCqwB!5$7K(@RZlwOZ0TL9x0{ zaI7$~1#3aGV=LB@m~e{K*}<{O1jh;!TeTMC6I-^H#01Cc?BG~sf@6h=EnN%piLG5r zVuE9Jc5tjR!Lh=`)~^Nm#1^n6F~PAqJ2+OE;84t`i(9 zOuD9QquJ@2vJIJViq+Y{vC0I;3X`rW+sG$fQ??-!9ILZ~W0eVx6((I%wvkV|rffqd zI96u|$0`#XD@?kkY$Km^P1%M_aIDS_j#VZ&R+w~6*+xF;nz9X<;8>j<9IH%ltT5@C zvel)q#;t6(i*QfrQnnEhS7?z##I@z*5OHldIYjD2J@=Bj93r-gEeHv2Jse8TkN00& z!xpqku&b;R^jeYF0=AsNvGr>~NI1L7;GkDQf?g{UTf3Iy5?i_!gap0H;GkDQf?g{U zTegM^@6ZtuS4xpX#I@bz5OM7`IYeBW zO%4%TrWS+*x7H(xEmI3xCD>I~33{zaY?)fl;Mg*?AS9e!WpL1|AVIGci7ivhafvNc z3qpckWpL1|AVIGci7ivhafvNc3qpckWpL1|AVIGci7ivhafvNc3qpckWpL1|AVIGc zi7ivhafvNc3qpckWpL1|AVIGci7ivxluP6Vz2n8DuaVw1qphs zNPL-Ej7xl(S`HHQDuaVw1qphsNPL-Ej7xl(S`HHQDuaVw1qphsNPL;vrd+arf!oyX z7psg@dTwAu#PwQV4iVROlS9O{+vE^&Z8kYX7KQlKXQG3U;MRI1v1MvOs|35sDnYLm zi7ivh864YfYC%XiyUO68S3!baD-v6#mg5pzrWS+*y~^OAS3!baE0V?MkNZ72V9V5U z^lHo0f{>tBaS3{@NNkx}&fwTGwIC#%US)95s~|zI6^SiV%W;V3#_Uv0aVu-l5o4XhyuH7bwh-#$iEG0NLgLzR zf{@r+wIn78_HZycJ3rorZM9m`Izh3zPH?O+u?1^EvtujPl9+Ic)!D(Z$^^#>6I-g?cHWrAaci7j0V@`8M!(mFx0x=wJcFtMdfVX_zkG;9iNDO-?ZZ*Z*64vtkOI98b0QnpR`z~OxoB3{Ki$T(~v>2_Drkliq&<3V}(iAlx;LST~oFp6Hc)@J2+OE;8f}sIGq7$1Z8Q<&2PYP1%M_5UdZ%-P~T^jgT}gEk;P1ZaO0*P4}D;lBQeE2uatJZO8<{%F(22$~IglC|1`Ajuj?d zQ?}9UbWPcYOgP2r?BG~sf@6hA*OYDKlddV-kO_{}*}<{O1jh=Kt|{BdCtXvvArl;{ zvx8%m362#eT~oG^Pr9aTLnb&@X9veB6C5i{x~6O+pL9*xhD>m*&JK=MCOB4@bWPdn zQaH|qo44uah@VuYmWrZYm)bk7+fsUK@nl-FzcCFrEt9ObWPcYOgP1gPcCq*GO3FQ$DVaf+3Hd_c5K&_ZO8=2iccQXp%Y}b@+$OOlVPi}CmGO3FQ$DVaf+3vZOt=`HuLejLf7$Ir8>5Pyx-E&4r znr=BGBwbUsArk~E$F;60+i;zrSY0PLR+w~6*+#R|HDwzz;S{U0gJYEmjuj?dQ?`*$ zx~6PHCOB4S2gfQC94kz^rfeghbWPcYOmM8u4vtkOI98Z+P1#01>6)?)nc!HR9UQAn zaI7%tnzD_2(luopGQqJrJ2+OE;8vY>jcH>I>E8Rq-n}FoSmjA+n5QbSe+dlt4wgLFlm~y z4SmuyWg9cWu{t|AR+->fVbU~Z8~UVa$~I<#V|8|LtTMr|!lY@+HuOo;lx@re$Lj3h zSY?7^g-O$tZRnGxDchI{j@8-0vC0I;3X`rW+n!t5%B^hoBP2~rixHBho6ZPH(>-T| zr0JG3Lee#58!|z#ay03hvJKY>iq&<3V}(iAlx;LST~oFp6Hc)@J2+OE;8bHHoN}6suBP31t zoDq_yTh0hc*OYC@q<#>=o^?&x>QXq~boMA#j<96)?)nc!HR9UQAnaI7%tnzCJTD_gylZG@!iw@GO+Leg~8 z86j!9=Zugv-Eu}qx~6PHCJ0uJCS6mu;W|OFx=wJcFzK4Ijb^87$~I)eDOP6($0`#X zD@?kkY$Km^P1%M_aIDS_j#VZ&R+w~6*+xF;nz9X<;8>j<9IH%ltT5@CvW6qsxpp$-RYXL4Vlyr;@GoG9IHOTvBIQl%2t(e?AWd;+mK29AdWq| z!m;WT94kz^rfgLi$BylqvJIKk58~LfYaFXS!Lh=mYsyxYaqQTxDcg`q{UDA#yTP&Q z6C5i{dZui5@)XIjV{Iwh<>GG%m*;QJ_PgEEfAQ*RpLYG%r~UN!V}AVS&u>xQA7B6F z;|I<^eg1uVE87@}t+dENV%v0bkl6N|93-|aCkKfyWeZ}0V83ij$J?+kWeZv-C|1`A zjuj@plr3j=d?{NH6Hc)@J2+OE;8F4d?{NH6CA6vgJYEmjuj@plr6_6 zzLYJ9369m-!LiB&#|jf)%C;?^?7u}`!er^#{?w!W?c$T&r&O|yh`2(F93rkQCx?h@ z!^t7y+HP`)*ebRlB)Ijk0iUnZum!CW>?*4Sy;dZ)fGuZmZ2ejg63(tNIOtW7px26I z@z?o&PYl@7wH&=(qE{Il^eRZuYeizq)^c28tJZ>$pjR0j^eRZuYeix!)^c283)X^= zpjR0j^eRZuYeiy<)pA^7Yt@2~pjR0j^eRZuYeiz~)VAZ2{pEglTkC-1*U4R-+Wv0w zF11q$)fSmlUeEs1#~>!IJtt|MxHg?6Cazs4iHWUPi-Lk>59gD$sU*a z5bezPAlexyh*qK4%C;n@*xI%zD2R4ud=Tvn6hx~~Y>iuzQ*4!66cj`|Gd_rR1`488 zD7M;dYfia*fos{4m~;&>M(d>O)-z(#b>|r|>ALZZn6%B^hETAq9#MR6G`g|Udbm>X z?956*v6 z8#$$I?ly#iXlKR;(au0Yv=+uUvBl(xCs5DKE5 z86QMD0|n74l(xBB)y5jPyIt(Ib-UY$i7UiNVp4fM_r|*5bezPAlexyh*qK4a*mAchD2R4ud=Tvn6hx~~Y`I&K zQ*60g6cj`|Gd_rR1`488D7M^fYfia-iQC<-7xA9do0*Q7xI&D55EIwdlf=Ze^CU5G zZ9GX#Y`I$$6fCPp6kF~VwNmix%t}GD3dNSYC5?|QcZ-6;d3I)e5bX>UM5|D2xm%J` zY`I$$6hu2SK8SV(3Zhjgw%jerDYo1#3JRi~86QMD0|n746kG0=-&WsPD zoq>XA6^bo)OLB@WcZ-68XlKR;(au0Yv8n?UM zFK%NeRlALlxH^m=B(9Ao2uWQ*RTNK^Ne~j(rW1t3R=OoIL9ma9lk?+k*w(ovtrHZh z>jcLN6I zvX}{UEDmg?TasvPsaq5jL_6aYM5|D2wcFOlr!Iyya<|*NrEu<~YPS&*SBDXV#C5_C zLgLzXf{@gqdhTU*K}c++TM`ol`*=86=jOJgb%J7bo#0quVvF2@X2;gJB{AU?tFwb+ zl?jd&CbqsU$S1bEEr|(^)!D(Z$^^#>6Ig?cHWrAaciLGi2@`){K zOJag!b#`#9GQqLJ#1^y#`NY<}q-%J|0k3dY{p# zm4atyRtln3D7L6AX?$!|TND(|voqs^XlI}xT7_aO+mf7OOWUHLAljMnL9{ba5UoP7 zfVPZ?!f_!32*^-#xSe+dlt4wgLFtMdR$q0-!FZolSfVPZ?!f_!32 z*^-#xSe+dlt4wgLFtMdg?cHWrAaci7jOd z@`){FOJag!b#`#9GQqLJ#FnxJ`NWp8B{9LVIy*R4nc!GqVoTYAd}2%4l9=FFogEyj zOmM6)v88N5KCz{2Nlb98&JK=MCOB4@*iyD_`9z+EUgK7_{cdpwJE3iuzQ*4o26cj`|Gd_rR1`488D2tiE{`}3^u3WxTpI*2)d`^;6Y^7Ti6hu2S zK8SV(3Zhjgw%ToLPT7~uj<9IH%ltT3?! zZ9zV<^=wH@aIDS_j#VZ&R+!jYwvG9urUPrFZ2ODdQWbYnH@A(L)Q{C}{Hb|g5);?9 zlf=Ze?IbaAZ97R!Y&}~P6fFCAKv}71i&`moc4nm@T7_bZ+LFe{R<%Vz;XFGtK8SV( z3Zhjgwz4hBDYmpN3JRi~86QMD0|n746kFbw-&WsPDoq>XA6^gBKOLB@W za*Kk3XlKR;(au0Yvmxoy8&j5&xAepm3g@86QMD0|n746kqNZXA6^bu+3v!AtcT0kT zXlKR;(au0Yv+EVpdi|r@j&w6I<05g?cHWrAaci7jXg z@`8M!(mFx0x=wJcFtMdfVPZ?!f_!32*^-#xSe+dlt4wgLFtMd<8}rHS3*5@Kzg>JL=A_UM5|C1pH$tSzd2j_BjSt0=Oj7B*0)7LL9{dD zgJ@@!hxs=U!Bnv`%{NJmYoJbK@B^X_~t&K*6$lM6q4qmZaG| zo}F1Kh*qIA&E3YLZJN6+K;b+)Gd_rR1`488C{1&>u~VAnZVONl?acTf+8HQ_R-rV_ z-NsI7n!7DPL9{dDgJ@@viPLxlW%tma*8i^OM-%EXT}H7&Okx53dNVZ1v$l+yCp$Ev@_#_XlI}x zT7}}v-GZFr%iWToAljMnL9{ba5UoP-+EF#>bbtB|+glJ2O6r zb_NQfRVcpPEyyXp+${+TqMaEZL^}fo(JB;Q?iS<}U+$I!1<}ro52Brcf@l?rFLw)a ziZ6Fdf`Vvg#s|^PKtZ$$#h1GUImMT|B|$;7GvkA3XP_Wjh2qQIHs+Mum$=<+cfYCJ zZN$VDVgxaTz&(5qAM5|DIxm(cq_;R-- zD4b_!#s|^PKtZ$$#h1GUImMT|B|$;7GvkA3XP_Wjh2qQIf}G;Z-IAan+L`e|v@=i; ztwQnTZb44*bZA3u2h>GSU&-oJhK@bT?0 zKcBxj+h6Xl??$V{we7qHlKY#Buiw0nNTv1M%kEx-$bPrVBC&;Tufb%URkGinvXV}&QLO0XdfSR!M<-iW>^i3tTgUb~I$33z z*fO@)V6xIO!LVZ3n{69*mEVbPwrtpyPA9g4?KO1rY$kW|^vLn^NlTZk?GG`}(&t=! z34XRzu#K6x5{vz14ine5lf=Ze?IbaAZ97R!Y#Cb=6fFCAkUcv;-i>|twFPZGdptX{ zQV^{|vE^(@qP6vGQBXL~&WsPDoq>XA6^gBCOLB^>YKww`XlKR;(au0Yv&COlr{F9Rea)$-ChIAN(*Jvl3mLvo7U_)3&oeZy~gnTz#p>=PqG|k z_4mMwjh|(o`R8%tL(0X*J-ZH+O?!5w&BvFzy@qJl*?fGZ+iNgcYoTn~vnwqWG^-o3 z?FzS7`eksHy=U>)+7o|fc9q|deWBZHcy=A7*lux)njSo>p0(ZM_DU;d6{Yw(w;ZMT zGPl=YveI)YwyWG;X?j*UrP%IrdnG9AC}q=68?19m@l|fGqm)$^im!5e4Ja!ulubWt zu+rXRyU^{GG`q^)W4qBU35qXrdkv-h!1&Zxx{hy=Stghkk|sZBqj*<@d?ODYi{e?lGX`|)pdeng^4Y13z{9< zMQ%wj<9IH%ltT3_lY(YM;zBCQZMWOhJC%-@*g}jTCbq5Tb(lQ+&D2Nh z#J2Oi29xK%m-+~aZQpqfB#-aP{nM9yX?w zWqezYFLg_TvT4n(wNf_j*>%Qe)1F;v^YNu_ui=z+HXmQ<_8LsqS}2?L>`Ds-&3^d> z`~3J%ZZ9{k*;V!)U+5O36kq7}8c^0z%GIVdyV7y?3eT!%uQu-4Ri1S7b#6II@nvqW z!DJ<+T;o|)%Js%QyN*&e?b&rsDZa|>b(FHoLh)5@uK{JHg@R^Pvo{;p>?*$@-)vg5 zE1gn&k=tu1<+)HUcK3Bcj-Oy!D$}0)rcaj}irf~M*aD0oCbq37h)JERXWw%d#KgAo z1TpbdZb?wEtR7~4ce*956g)e#QV^{|@r76Qcq(awwyqMd<)XcdYtbqjKeFLg_Tf@o*P2hq+zL9`0R*SZBc#TUCJK|!=L;;Y?)oZ_q9lAs{knejoiGf)t%LhsZ9Q!(v`d2EjAsOkM|!f zhk{p6`?Tx7KJ7UdVH|3=5fWF25ro9G@dP1pZ973oT$@f1l0_>%H78DDf?#DEwsmev z>jcH>I>E8R#1^>)&5o^cOJc$)R%ZvtDia(lOl*BykWXxRTM`o-tFwb+l?jd&CbqOK z$S1b4Er|(^)!D(Z$^^#>6I<05g?cHWrAaci7jXg@`DrvgujF2?lb4Ey-ZaE_)T~oFp z69g+qlddV-aGju7T_-qJm~>6qMzhm3Wg9Z#6sxm?W0eVx6((I%wvkV|rffqdI96u| z$0`#XD@?kkY$Km^P1%M_aIDS_j#VZ&R+w~6*+xF;nz9X<;8>j<9IH%ltT5@CvW@N$Ui~>N>%(!o-%c1q~n3^EYSpqKx`E7l+S@f`Vvg#s|^PKtZ$$#TL3HImK4GML|KdGvkA3XP_Wjg<`AS zw&s+}7r41CiAmQGW3*1XZapI=U3Z=lldc=jh)LVrZ3qR+>Jg=F?lxR0cy?x`AXi#s|^PKtZ$$rETsua!T9WZ3qR?&WsPDoq>XA z6-wLOt!m@=KAEMz&3NkHZ`>}@J*msxMo3&8Mi7#UtNsN~!&Tzic7j%kYtsorVk_N} zm>}54!^t|=x+Sd>6szk5#|jf$Kb%J7bo#0quVoTYAX2+JYB{AU?tFwb+l?jd&CbpC<$S1axEr|(^ z)!D(Z$^^#>6I;p_g?cHWrAaci7jOd@`){FOJag!b#`#9GQqLJ#FnxJ z`NWp8B{9LVIy*R4nc!GqVoTXJ=94-eYuw6qxA;WNN!`jeLSic|a*)_Iog5@}1wHqo zx}d?aZ8N>%(!o-)d<;;#RWeZ}$DOP6($0`#XD@=ST zTaHhBDO(T|9ILZ~W0eVx6()-oc@Moj#VZ&R+!jQwjiI_Qnn-}I96u|$0`#XD@+!jRBrfOfh}bVa;z<7OJag! z)h9SsnAlRbpxLpdY)MQw$Lj3hSY?7^g^4X?3-XCAWlLg$V|8|LtTMr|!o-%cZOkWi zDXelU+ug1NbM>04lSt#csEumTk<$m0Bb*acw(EOkCSe5|iqh=jLolOl&<{ z6cjA`ctANjKi-XPMO)NL!Lu_f1<@)LThx{`KDMeY3JT}hnejoiGf)t%La~)?Nlvk) zZBbAV?acTf+8HQ_R-xGPwj`(6`nD)2h<0Xt5bX>UM5|D2ja!maY>`_O6hu2SK8SV( z3Zhjgw$LrfDYnur3JRi~86QMD0|n746kF}KEvMYwRBar;z`oR|J@vMw{cdp|JE5!F z#!OrxMiP^{hMt=-B{6aBJV{Jk8&47wTkaMG1vvr-VPLb2s;N#kS7 z-J+myo}C#VL^}fo(JB;M?v~^fTkaMG1<}ro52Brcf@l?rEq6m`}|*!bH>M*y9GhvJUcT!h;{}FqE#ry+%3l`#@sCk3Zk7EA4EF?1<@)L zWA2vY6l3ld1O?H~j1Qunfr4liiZOS~af&f_3xa}ZXT}H7&Okx53dNYaM(d>O)-z(#b>|r|>ALZZ zn6%B^hETAq9#PunZo`#=XJ=LlqE#qubGOm>w9VayP&m)dj1Qunfr4liO55CRALld zm~`EFMohYHJR>G;bGIRs`avxF=B#b*Ru{wZ*0aa6dYIiKT7}X!cN>k59CZ=xo3jhf zvoqs^XlI}xT7}X!cN>YeZSFRNf@o*P2hq+zL9_~`ZSFR5O55CR2nEs3j1Qunfr4li zO55CRK?ly$Nd3I)e5bX>UM5|ER=58aWw9VayP!R3R_#oODD2P^} zw9VZ{PHCIF4WS^~nejoiGf)t%LTQ`3jhxapcN;=Mv@_#_XlI}xT7}X!cN;mSZSFRN zf@o*P2hq+zL9_~`ZSGdJaeQ@3*EcgAG3gp&jF{Aqbx8p0_kBD&U3Z=lldc=jh)LVr zZ3qR+>Jg=F?lxR0cy?x`AXA z+ctL_La84_v~SL?5$%jq5UoOKo4ZwQ9E-MX?ly!{KZt1GoZTSW8K)pxh0->6yXJN` zx!rBVq-%&VV$yZ%88PX)^Ng5u-FQY!+U9OUC|FjHC~b4M;Yz`?Gb;trDwMXl+h}~+ z=59kMoM&go2hq+zL9_~`ZSFR5O55CR2nEs3j1Qunfr4liO55CRMohYHJtHRdV_oX=`h6e!r0d2rV$wEu8$!XddPHfPyA4-L{UDw_ zydyiJ)$wVYyVVIf-jUnpZbK-XXJ^Kz5<|3yEAJ4ku9UX9Tb+<&(YDRqhENdg%=lDd zi1u*h9ir8h(l&Ri6LKutwz=C73Zk7EpGpkT9dM4$ALldm~`EFMohYHJR>G;bGIQBEUQP9wz=DIrQq3_m4av$O55CRG(K%}w;>eHvoqs^ zXlI}xT7}X!cN;mSZSFRNf@o*P2hq+zL9_~`ZSFR5O55CR2nEs3j1Qunfr4liO55CR z{l1SK+ctL_Lcy|nL}{D54Oa@DomnZB7@|Gg#KdV< zS4!L5txm}CmVCi^c4nm@+8HQy86n!kO-zVZS4!L5txm|XXfF}%%t}GDGf?U>LbQjQ zm=LY5l(xBBoseVEULo3XA6-w9KZRnJ)x!V{DqMaEZL^}fo(JGXtx!dJ!osi?_ zXl=RM<>D_4m*;QJ_PgEEfAQ*RpLYG%r~UNh2YvWIe}22e?vJnk^6`V`pFaOyZg(3q zv4t2xOl(_E5R*Dr&wkA(h>30E31Z^Q-IAbS*)KcS`SEV-%iWSz3Z9)=DTr2~_;R

XA6^cAf$*(?#cX;JQJF`*{?FW_%Fs3=~AGP<*-D#+-8dEd+a{GI@c0=>-4OQ~vGZGvFsxyN!^zp0y4_;@WtE zkhr#;ASA9$CkTnHbW37_U}YP&b#6)P1jXt)!Lh=`7P$q@j;(P^V!|m_X9veB6C5i{ z7Js{M_&k9vZwqqlC63kE!LiB&#|jf$+7{#!TiKSx1jp*^;8ncOAS@hLQVl*2+e-}uOX zYqAD~S<7Ag8=1#>Z}D^PxQElDbXnc>C~bby9Yscu(q(zmqjdR6)1$QcNq77miyo!R z^1kkPBBMv?vb^b0y8NW+QM&x3=~24;r0G$*{G{no+We$D{=(6!M|r$CgdXKE&wu?x zkJ7QbyX@$>j?a~LT-+VkHa*H=KWkB2(WA8aNp}<(JxZ75O^?#$Cryvi<|p0pcPx68 zF3X!9rOQv6`%$|5q`4oZ%TJp7QQG{ZTOLDOK3&xDZ7k?fx`^zi<4m{eQEur`zP^di zVXDvQQ98b4Pmj{%)$5LnyW<{CkJ9CcN{`a!C*4tG^eA1HH$6(1pENy6o1b*c7Xmu| zlEwS=beU=HN9ppD=6;keKWXkq>GG52ev~dhY3@hq@{^`VY4elr_!Mw-kMisL-~RFT zm*4*Hzl}PSpFh6;^ywdefBWgb|MvI8=k3dzTs-{G!{>*OZ~yDz-G4vKeE8krV}E^x z|K+3q&->s1`)?n9e*bai{QmLd`?o*+`1C0k|N4}7k00Lt{L{xz$A9^spZ`ZsU-0)Y|KFcq z{$}${OkMw`}WKK zaQEfE`^VQWF@7-DUqA8nAI!};J($C44`%=EKc>6Y9?T2Y%)>c7n5)BiJ)B#e-hYbY z%arOTtT*0Qx9ctA7p$Da{_338&SC$T^jCk7f7Qi@{doy+|CiQ|o6ge(NzZ_jDCuC8}~$vqYBUp4dX;c$Ic3+U+q zoz=PL_<(NCYUH0D&|li!S6D+gXEkI`gwAbI-<;JRe|xxlII~54_i$#5`tISZ4y{i= zvNM~qyNC0dvfJIwU)nnALGv$fuD92_^IAj42XtP?kK+USOFuIEUUqx9tzYw1f8V%0 zKA`gwwd?D1GvxJkJ&(TXLFDPDc71j$`}*7rd3}9W z2kg&3wZC*+tBdfj{!;Y$5uMd}?f8fe_vf~b4)=fQr&eWV_^BQ4&u#C&zp0D!wGDZH z^OvT-emj5tr*^eFuP2R;JO6%H7pL#1_JRQA_=v6#XLj;GKBB+0jw)coI=a6-Gecfq z-+dQDeomVH>z}V7UcUR=KRz5&xuq`d&rh3;;{N>DkB`4SMQ_gtrk?%9p0oH*zgRuY z{iR1X{9@nw$m&x2OOI^$#lH2C)g||r9@+4Ved{BuoAY0KWWz7^t&eP9b@Z1W+W4z| z^J5#X+ix9DiNGe`={G;N;mZBy$2R^J5#X-fw{(OkL}7j z3*)c$&5zAG3s=Lh_RWuNxO%_!XL>dKYTx|WhO76RAKUP&ed}YB`e|aMZ+&dzulCK4 zZ8%xq{Md$H?OPw4yk7E`xQ)NsH$S%FWPK|)iJKjU$@=EU zHvDSe{Md%8_nRNv@T-06V>?*)tnpX-=EpYtDSRup!|9d~ktN-c$^4q@p)v90pdeyIfv+7qrtoqe&SN-aDtA6$SRloYf3%`1OYW8#{ zPNwOlKh|e{`Ir7&pZVoq`h$Jumw)L`_L*OP-5>4Mb9STCRq-!VKJU?Ay;Q`{{PJSr ze?6D{`0KBa|M>OY@ei+mdHg?5f1drH|N6-Peti4ik3T;?eeC$R)lVP!GCSkHdHy#C zwb|#PPyQo+fBf+H%l`P^ssH=>Iag2n*1f!bQ$Owc>C<+LpLX-SPdohiXE1n&GIwXf?-Ce-CJmqyFmA9EZFF(VYh}{nsnQpZ6B|YxnN!1D#IS z2R@zfke9KV&x^ct)}05Z)9Jk6(-B^1!DP9+Chx_s4;;pQ^J(Qt1W;b^$EjBs>J$A&zR>E!6q zF&!JO4|F&kEflsINf={PB z@acp{$8>DukB;fskOw}U&I>-B^1!DP9v#!Mkv}@7V?!SJbUH8ibjkytPIz=o$436> zn2rs3;M3{6;L|A&d^+LLF&(Qf;jfX7T`gZHb}qM$jd0YDy|knY!Qt9mLU6eDmJl4S zttAA9EgehYflMby4_i8xvOdu1bba8{2@hL37BVlkbS#Ajr_<@Y;L|A&d^+J_OUFX| zVN1tSc;M6Nyx`L*4}3b|VN1tC{9#MSQh4Cg>Ac|6DGz));bBY1Li}M%$5MFU)9Jk6 z(O>jR%oc-Ydhka@ACV<|j1olfTk zpH6w;(+Lk-Iu_y&TRN7)1D{Uk1)olN;L`~YTRIlv4_i8x!ULa9=LMfmdEnCt4_i7G z;tyLomcj#{PUi)mPI=(d2@hL37UBuZxORr$dpyAqBGHAHAl?)oTYAgf?9zES-ZOvH7 z>cFIv)qzS+aoB>fjA60$Vj(yP=(o-C^RxHCEwp1(x2P&Nm3o4!9K&7WR zY?)YwJ8YF$2o6*_85UGJ!GTIoao7s640qT9u@D@nbTTZcbb6dvy0GoIf#_xaV#x`@`ZMbuL}l2#2d@2*Kgn zSVC~PHkJ?^u8k!Ghpi1u;ekviTdl1QOIaW2bh4b+Z5DS?XTOpRhgVX7BUhwIZ z2R@zfuvKCq{;*|YDLnA$bYAf3lm|YY@UW#~A^xznVktcE>2zN3>68aPo$#>rVj=#p z1!E~Z@ac43@adEXKArGbd_lh1Yx}lpEX1d`_;fli_;ktxpH6t#%CQakqmIOBN#{z( z?(Ubq(z%plBO0#QA%li%YssKd*Ud}sSIeN`+Ey}X*s8G*9C-9}kF_;pA*%zEPF4ph zJ;h-Q#xjP*){BMU;7mFh7F0UHfl5zt*jlj+ci2*~5FDs10??=>!KVJ;h-w#4_Ar3&cWjpwh{(pwbBrRCvOoC4+`*Q^}xV%fdo%;L+0~ zhb;>WSsj>kvN}-dDUQW+W!GTIA z!-7gDI8f;+4qFzM;SO6C7J>tnPKE`QPH>>oQyjJ|EW;hPEGz^EDxC}qDxKg!rKdP- zSy+ZUY*|%fdo%a3-A$ z3o4!9K&7WRY*|=_J8W542o6*_85UGJ!GTIoaoDo340qVFun-)mbTTZcbbCWTPjT3?unc$Dvak>wsB|(csC0q@m7e0TWntTK$HRBH zEo^_kTPn=Xr3)M3aPI{SjfEC3b7O(oKC0nf={PB@acqytr83Ihb4b+Z8Vm7#$Ko?7=aP;Makz4a`wSee-6aHvYjX*~;o4h5aEMr5 ze9c;hJod4b-^9SiY?tsP6@flsINf={PB@acqytsUEtKlb0@+OdcGr3me8(yBT`II9!`c2oBfY5`x3EwS?fXrDG{Pkm=;;VN1tS)(1MBt`B@V;bBY1 zLgvMmj-~M6bUK|Ed^+WUPbWNV=~#$AZ0T4E4}3bE7koP9flntqZ0T5tKWyn(3J-ib zofmvM<$+HpJZ$M$h(B!USPBn(I-M7MI^}^+Cp>KFScpGt=~xO6d^(*Md^+WUPbWNV z>DYGsadoIOu|_*~x0!Zq%)`|?r0{U*jlm}AV{^03{ovXkm?kO zttU%yh%G3K0fJP^$RO1M1gTDe*rKu&huEsJ7$8Wsj0{pOK#=Mbh^;J3afmG~ivfaE z%g7+r0tBf}f!Ol0Eji@wJG`sq?PlK9^6D_+;ff(rc(}Hh6dtY}CWVJB|j$N$yUVFbigL&N}aZ89-HT$@Y`kUCB; zz0oZOh^;ZpA%a$q8`0^;Ypcw1)(B#)YXq|vBDT;hW@>DuSq>3StaWNIYbAnN3lUpw z7UL0HZk9s?v(~A>td$66Ekta|S&T<)%~=i+%vz@gvsNOQwGgp&XE7eJg=aZLFl(I} z%vyM8Ei@w$TOH>a!TL-eT4|HJG&$!K{Udtw7t9M;^Y$ zS)Y@v(^tZmwvp#Ak`@lTVJ-Jk&%sLjl67swRm;e*_6yfAg&rB28iqA9s|U+$;1G0Z89-HY>inC z5wu!Lci#8q8XWVAev!R-46m#Fm@o5W%c< zYA|ahf>{d@TXGiT5nFSXLj<$dsllw32xcurY~5LmM{MC)4iU^+rv|fDBAB%hk(bC{ z{H{eQuTI$Nvlz45^0OQwn6-KYvlb$@0&P=Mv;Ph^ob7ju-NpGNXd@uDRw4z6?FgR& z#J10*0I_W|DL@ug_}sr9iLvS)t(K#RFF}i$8pK-H2xcurd(pS@ zN(8eOBEAGI#Us81ErtkYty6vEWQas{I z&|-*S);cwqwGzRsg@`XfOYw*=L5m@RS?knb)=C7k79zd`ZA%`hFaNJ`3)=nSi-YG= zppAIckG-`1PT}F&W>R>#HklM2t}P~ohpj$~0fJGd7qQyfvzQfvQ_BiLs#73~XUpd% zy(uGO>&{|;a84~FgH#I;q&fv+YtB*}VoT0qfFRW}GDx)mL8?EbO5L;dr zvqErcSs_Su3dEL|rHqU%FN*=fIkk)oQY}D`>J*4AFH3QVEia1!f>g`MAk_i{sZN2| z^0E|%*z&R%AV{^03{ovXkm?kOEiX%Ph%GOR0fJP^$RO1M1gTDe*z&RzhuHG67$8Ws zj0{pOK#=Mbh%GPMl0&K{R=K@wx1GFf#KRRs+=uX}tZFp)?0Z8(c(`_$l=b1-U{ZM4 z^0F8p7mPJ!6+vJ{8d^0F8p zNVSX%QY}D`>J*4AFH3QVEia1!f>g`MAk_i{sZN2|^0E|%*z&R%AV{^03{ovXkm?kO zEiX%Ph%GOR0fJP^$RO1M1gTDe*z&S1Ipk3864*TBioHRw9_S5LtX;^tpec8RHRKbCyE{v(~A>td$66EktbHS&T<) z;aLt5%vz@gvsNOQwGgqzXE7eJ)n_?GFl(I}%vy6oC6J<>5jTOfj2>(pS@N(8eO zA{`U7u}3;4XbVIzYn>X*T8UuRLZoAYHugxz1Z{x`X020$St}9DT8Q*a(5}D7Eoj$^ z&$OJ&Eog`R2#BqfNC9HoXi|XK_L&qQwrwT_h%Z5lA%a%RQN)*^#jFv;TGt3>Ekt|? zTFTV;60{g1oLKABVAe_mvlb%01TDoQz633X2xhHQgIOyP%vy*nJ~8_2ThLNG;!DtC zh+x(_HJG&$!K{UdFF{N3h%Z5lA%a=!)L_<11hW<*z634BBfbPJh6rY@Q-fJ65zJbM z_!6`&d8Ddhm0QrRcDqu#)%(NFCqWwlv9%H@Kq{>IV}14$XemH!`%DTD+cuK|#FwDO z5J9WuDB?@dV%7*^t!o6c79zd`EoEwa30e#hPONomFl!}(Sql+gf|lYDUxF4x1hdwu z!K{@CW-UZ~30jIrd{d@UxKzJj~q%Hwn~Dwd)Q8bHUeU6B~pOcHkuS5wtXfAh;5rm z0pd&0Vu+yCauo3;XfbO9vDP($Sql+gf|fEhz633X2q)G$HJG&$!K{UdFF{N3h%Z5l zA%a=!)L_<11hW<*z634BBfbPJh6rY@Q-fJ65zJbM_!6`fkN6U_7$TUpP7P+QL@;Y1 z;!Ds{JmO2xVu)bYIyIQJ62Yv6h%Z6gl1J)btdXGIZRirT5f4`?k;22Z&7|X;1L8?sTLqebqd55nWZ?y)|kZrL8@hBkZJ*fRHr~}ec5&#a(7!7!Rd?LOV^S;`z;i& zH`AAmfw*djeF_lQCKCh1waLT)seXBBau)-{)|llGL93;7w^e32YXq^@HG)|S5nE^$ zGc~r-EQbgu);cwqwGzRsg@~;-i}8ppH_IV{S?knb)=C7k79zIfEXE_Y<}8N@X020$ zSt}9DT8P-Xvlx%q!m}JAn6*v~X01dpYawEb&tg1ctIu+XVAeV{n6(nYtc8fJK--i@ z9=^+)Y%V_2a((&cV!vA~)oz~mX@@^Q?T3p$|M}zVclrC5fBE#W%a5Oa`}zIbcaI<5 z{`BMVe|2{|0%B_=b|FA)8%+uj+dh*5#J0_(0P!VgF+|X6Ig0oaw3s!5SnC?Wtc8d# zK}(q$UxF4xgcEC>8q8XWVAev!m!PG1#FwDO5W%c{fZ#peN^{b#-@9`Pk; zF+?zHof^zqiD1@3#FwC@c*K{W#Sp=)b!sqcC4yNC5nqCq;t^kh7DEKH)~Ug)l?Y}n zM0^R_hCCwQ^cuII-Q6$Ur*<|4+K`7Ul}O>?+GbLCxHg#-9&^ivfaBrx&r> z+OwDyf>X;1L8?sTLqebqd55nWZ?y)|kZr zL8@hBkZJ*fRHr~BEqdu2!fnYR`>%I@eZKK)jl67sxA+?1`SfKYAg&rB28e5ui2>r; zWMY80HklY8w#F=n2wE+ryR9yh%6p5pM6_I%y|{F)-}S3wGzRsl?Y}n zL~ONL%z4C?o8=I}taWNIYbAnN3lUp#7UL0HbCyE{v(~A>td$66EktbHS&T<);aLt5 z%vz@gvsNOQwGgqzXE7eJ)n_?GFl(I}%vyx612mHt~eV3sULf3 zqqt82V%um^fY|n#6d<;3CIyHuL5m@RR?AVum!QS05yV>82xcurdw$G#hv28OcKzs>W3=y{d@UxJq65nqB9Lj<$dsllw32xcurd6@yLEB z*8;3^4chf)8ngu>wpt>Fh;6TlA!6HWVu;xGniwL!5G@A@Zauyg_T}mR>r2scRta{M zRf1kC5?_rLGdR8+Ee8o_R~a1iDoD_4MdC}+VqD^D(sGcXR~a1iDoD_4MdItyVqD@2 z({hlYR~a1iDoD_4MdFLoVqD^@({hlYR~a1iDoD_4MdB;eVqD@&)N+uZR~a1iDoD_4 zMdHiUHszATcepC;ezD59zI=1BU(Wk(p7&{oKR@k<KYx7vCV&6(FP}bg`SH_l zKfizb?(xIhpML!Ozq%R$ag`J?KwO(m3=r2=69dGx(Zm2*bm9K!p{1K8ua2LSLj82xcurY;ju5)Y#gz93q@p>(pS@N(8eOBDO9q#v`^YEr$qZty6}7?0SBv>YOswN4FYtwbIO%TfsO+*sma@x{S2O3+3?T&+Y55Z6W%1Ej8=|NZ#( zzaM{o{Po@Yw|{^9@c2u+k#TJ^F)PHDpyd!jtK}$SyCN-TjUd*#Mlfq3vUtjDxB1!< zv>1=b&Cu-+XjhoEdIYl;BDMrA#v`@_Er$rt1=Oj*td$66EktYyT8u|*30e*j%vz@g zvsNOQwGgo-XfYnKC1^QBFl(I}%vy6xGniJ;YT6zQ3u4c7=_t!o6c z79u?pw9(Y`OwfizII-5L!K{@CW-UZ|CTJs%^i0r(L@;Ze8q8XWVAevUXM#5JNY4aq zNCdOisllw32xcurdM0QikMvB?hD0!Hof^zqiD1@3q-TOQ@<`7FZAb*O)~Ug)l?Y}n zM0zG@bs4OYpl#|}xDgOnD-i?4wb8@?aqTlPK zYh5FlwGgo-Xfab`OVDzNaAK`fgIOyP%vy-p60{hP*b=lHBAB&K4Q8!GFl!-VOVDCG zVoT6+h+x(_HJG&$!K{UdEkTR%h%G_OA%a=!)L_<11hW<*wgfH4Ben!BhX`h^Q-fJ6 z5zJbM*b=l2dF1N8zS-s)3EKW@SN}7$L^+#x;v5rk#S%G0Tw6^J5!Y6eL&UY!+u@W%hUbWR-y&166`9g1ie-ywiqpEaBMYN5E9O=GC1f}kf7I!#8#x`xWtyE z1tCGNGC1f}kf7I!#FnMyxWv|_1tCGNGC1f}kf7I!#MY+exWpEx1tCGNGC1f}kf7I! z#1^RKxWrbd1tCGNGC1f}kf7I!#8#1=rI1=r60{s5n6*v~X01dpYawDw&^G0fs){vk zLAzgkqUDTkK^ptd$66Ekt|?T8c+}30e#h%vz@gvsNOQwGi>mZ0Sj;mlg62D4To zn6(hGC1^1ou_b6ZL@;Ze8q8XWVAev!mY{9QBXuxVxdrWhS3?6h-<6KAyOymr4?EZ5!Y6eL&Vmh1tGz$7v)4^E75{h33ioLf?g{U zTa1=7IJO!s2nlCb865N~NYHCVVk^>eTw+Vof{>tB865N~NYHCVV$0HUTw?3ef{>tB z865N~NYHCVVr$cKTw;sUf{>tB865N~NYHCVVhhxATw*KKf{>tB865N~NYHCVVyo1) zSyYZoY?)dR z67(vAgI)y*daX!onc9Y2a<#ju3t^p1Z8Nv2EfDdg6e&b}+f51)-)@saq&nv1ac2q< zW2P2^1h>{Bi7``)StZz2Rtb8oNQ{|U%HS9?wHPFvU1f04s~|zI6^SuZOL2)YQ;R`@ zUS)95s~|zI6^SuZOL2)YQ;R`@US)95s~|zI6^SuZOL2)YQ;R`@US)95s~|zI6^SuZ zOL2)YQ;R`@US)95s~|zI6^SuZ+mcHj{^N%ui7)?LlBtb|^h_y6M0##FBO*O_n-P(o zo6U$w(@brM1h*b{;mQ8nG*cU{66`9g1ie-yO*6I8;55zDhDbQO%HW_^L4sZ@lBSv3 z$R$lPwILGpDuaVw1qphsNSbD9BbPMI)P_jVs|*f$6(s1jB59hbjanw;6&XKF(tEmMjik(S%dkVwnjW=N#v zW-}zxGgBKQ!L9X3(lb*VuM+Gks|3APBt0{=;o$Vl)W%3SyUO68S3!baE0Ug>+R!CE zGqo`i^eTgcUIhtytw?%iYD1Uw%+$t6(5nm%dKDz-wIY$JYVqbC@?D?4(AP6l8zVul zGC1f}kf7I!q-Ul!bV<)lZHxrH%HW_^L4sZ@lBSv3p4-%Ze*gB}mL9c=Yy;dYmGqoBAoc@u; zt|V+d&hsN8^Hhx>Owe`>mIw-gX zRwPX`wb9@-&D4fSIJ?T=pjSbHUMrHOncB!DO*6G267(vAgI)y*daX#BW@;msG|kk8 zNYJYc4tfWB0;Y*IOtW7px26|X{I)ENz+Vihy=aL;GkDQf?g|< zrkUDxNzqPU6YiO*jfhn9_DCs4M0##FBO*O_n-P(oo6U$w(@brM1h>{BNz+VixJt0A ztP=EEku=TJMuXEdQyU`T>?(tUUIhtytw@??Y9p64&D4fS(5nm%dKDz-wIXSnsf}FH zG*cTQL9a46=v9!Q*NUWRrZ#d((@brM1ii}OpjSbHUMrHOncB!DO*6G267(vAgI)y* zdaX#BW@=RzWNITKJyVJik@~S7;a>f&Pp78mZZjg%bF&!{X_~1Gk>J*PBx#zd4Oa5Fnr3Q4B=v*n^%}j3OVDdY z(lk@6x;WKq(@brMq<#>+-k?`;33{zanr3QM7pHn{nyC$u)DNQ919}ygpx26|X{J_n zajMs*nc5IZ{UCb1MX%x#^jeWL&D0LuruOsux9=W5y#49N%QqKSyTgb`&y-?Bq~~@s zBGPlW84>BZ*^G!Z&D4fSaBDr1G|kk8s|35sDnYLmNz+ViG&oH&wILGDt};02Rgj?9 zilk|#HgZYROl^n+y~^OAS3!baE0U&}+Q=nMGqoWS^eTgcUIhtytw@??Y9p64&D4fS z(5nm%dKDz-wIXSnsf}FHG*cTQL9a46=v9!Q*NUWRrdDU;v;yw=rk^7sJyVJik)GSl zh)Dfdk50UP*QXxox!H_}G|kk8NN{UCk~GcKhO4B05WDWtYaN`XnOdEV(>=IprZz;v z*;NLo@f10nyC$upjR23 z$_u^Tpx3%enr3QsHcs{0G*cTQL9a46l^1$Fpx3%enr3QsHcs{0G*cTQL9a46l^1%w zMXz<0G|klRxJ|9xrZytdGo=_2>ABsEi1gfTMnrmUHX|ZUGqoWS+**$$O*6IOD#5O@ zO3-UX(lk>W4NlWcZHR=ks|*f$6(s1jB59hbjaIP^XG$?5(sR2R5$UzQ%RIg1lwILGpT3vF3UIj^ALg=+=rdAiisa~6AYC|OGwYubhUIj^A zLg=+=rdAiisa~6AYC|OGwYuaMy$X`LgwSitOznDCb#ba!Tc&os_^ZM7<(rHBZnyMb zym{WI9sc~ZACBw(^FM!ly)*9jFaPrCBbOgP{WiHxZA`?LQp6Cc>*vKHi6LU!ZDNSn zHk%kCzDz9#32uGbk52brU#6C`O0cV}67*V;_%gMa!SQ8kIY>CW%HW_^L4sZ@5?`hk z;}Tz{mV*Sn%HW_^L4sZ@5?`hk;}Tz{mV*Sn%HW_^L4sZ@lEv4e_QzL`mR^XudU}N^ z#wET?Ee8pDmBB%;f&{%*B)&{7#wET?Ee8pDmBB%;f&{%*B)&{-Q!Y7t4Y-a(rZ1B( zoz#9G$~ zW-UZ)aazpO*xIxlBAi(3)L_<11hW<*i@(3OdnUk^rNx-_8nf1^!K{@CW-UZ)Nm`6Y zY(-iQ5zJbr2D4Ton6(hG)o3vuvBhXPL@;Ze8q8XWVAev!7NW&?#MYtZ5W%c{d@TZ6V8kK7&(@*V@HSGBkrwEg|!9cbqgp)DbCEtfb(!A}wj1pjcfeI98b0y0oC#v6X2_OgP2r?BG~sf@6h=txyZ{iLFsfVuE9J zc5tjR!Lh=`)~W^h#8#^%F~PAqJ2+OE;8iV(ZqDnBZ8Q9UQAnaI7$~^=m;s zu@!7dOmM8u4vtkOI98b0O16#pjcLNlddV-Xm+}$Y(plTVs&6I;p_g?cHWrAaci7jOd z@`){FOJag!b#`#9GQqLJ#FnxJ`NWp8B{9LVIy*R4nc!GqVoTXJ=97o-aVy(lk?uLY z!RQEytF+kXAaQLvK}cMCP7o5;mJ@`;ma-)=L9lW(v88ND>jcH>I>E8R#FnxJ&5kW) zOJc$)R%ZvtDia(lOl&DzkWXwWTM`o-tFwb+l?jd&CbpC<$S1axEr|(^)!D(Z$^^#> z6I;p_g?cHWrAaci7jOd@`){FOJag!b#`#9GQqLJWHGjS?zKVN@`*e} zvc|1!4~rYv8I^29BCgOPhlp#-$syv}aB_&awwoLxwu&tX32r?eN-j_LUt7Z#v`Vn6 ztP=EEk=O#ZoWZg6Ye7gjyUO68S3!baD-v6~mg5pzx)y{4y~^OAS3!baD-wAI@})P} z=eWdHtpy=LuQE93Rgj?9io{l|<+#KatOX%KuQE93Rgj?9ie&M%{^y1iIWDoaYC%ZQ zs|*f$6(s1jBC&O9+j7bNyWF_8xObgXr#3?3YAN=I7$mL@CkTma!wEv-+Hiu9*jlwD zCJ6R;Fj=WqOIjx=R@VuR6(+V|EogRZ#aa>*PO&;WI98e9SYcwT)`EOu%hr;Z;8>j< z9IH%ltT3^qYe7D-wQEUCaIDS_j#VZ&R+!lOwIH9^0=6V3I96u|$0`#XD@+z&^?q)| z7UUCK#g@bb$Lj3hSY?7^g^8_X+n7&kI&ga9%dTW@Fa68EyZfcDbWSDPh)DeyL|j`= z&KhxTI5|XI+f5D;Tg4WH1h*a!CF|U{7PLyRtE>|AT9McSww%GS^=m;$IJ?T=pjSbH zUMmt?yO!e;Te=p61ii}OpjSbHUMmt?wwB`(TeTL11ii}OpjSbHUMmt?v6kZ!Td)>{ z1ii}OpjSbHUMmt?td`>vTdNj?1ii}OpjSbHUMmt?r?xGZT$dDWjZE!f@nyqv%G5?g zTq#8k5!ZH;L&UY)}mqWz0+vE^&Z8kYXY?)dR65LvkB(_W~Xq8}BStaPT zBC%y^IfG-%)Pj(3c9p?FuYv@RJuYv@RJuYv@UgYjHJg-;HZADzUvWXwcxG*E%@1fGua0*!r~~B%EDka5%gQ67*V;*xI!m zm)O#^ASCEj1_!+g67*V;EdH|g>??s5ztUEocePb(IeNW8uQE93Rgj?9io{l|<+#Ka ztOX%KuQE93Rgj?9io_PH<+#Mwss$lIuQE93Rgj?9ip18bZObKh-{Ho!hx;vExHcl< zN-1)PxVD=dBCg#ghlp#l$suCP)Pj)U)_NqdWokjI1iQ*AL9Z2wEmO-G99yOqgoLxJ z3=Vn~BKhSpr4%_tT-!|!5!Y^$L!?gBOYfJ!I@a)`Kgn;asp%_fJ4EmI3Z zf?Mm6#FnWAtrF}is|3APB(_W~XK-wpS`ZS>t};02Rgj?9io}+w<+#L_sRbcHuQE93 zRgj?9io}+w<+#L_sRbcHuQE93Rgj?9io}+w<+#L_sRbcHuQE93Rgj?9io}+w<+#L_ zsRbcHuQE93Rgj?9io}+wZObK97i-+6w%;x8UFY+zpLY)jcLN6I-wrG&{CpEr|)ISe+dlt4wgL zFtJr@K|Zl%Ye`IStj-RORVFxAnAp;_AfMRUwIn7uR%ZvtDia(lOl+w*sPQwI$lVGVt8KGYE-m!wEuSYt@pN zAlT!+fSY?7^g^8_N3-XCATT5bsV|8|L ztTMr|!o-%Y1^L9*t|c+Su{t|AR+->fVPfmof_!2N*piswSe+dlt4wgLFtJ5!K|Zln zY)MRTtj-RORVFxAnAl3Tjrrv6JKVUo-z`=f=ajOIkl0F#93-|)CkKgb&&fez+j4S{ zEK2d&#bXc?1S>}qjcLN6JN@f<5*wH7Q}>8tj-RORVFxAnD|n* z9H017wjd@rR%ZvtDia(lOnfO@j!%3kTM!c*tFwb+l?jd&Cccy{$0xp&Er6JN@f;}c)X7Q_U{>g?cHWrAaci7#c_mQU(ZSmRc<`^6U&&*>HP5fN8tkwe6_ z<>U}?Z8$kZ>O{TtlDZrsi%HRQE4Cmcxb=7_S?SF~V>V!0!WJ|*=(P^c;&Sh&S6jc9 zGdNeAU1e}Myb2QZT9Me=wH%k&(zPHY=v4*>y$TZaT9MeYwH%k&sy$TZa zT9Md_wH%k&g0&zd=v4*>y$TZaT9MddwH%k&TD2e~=v4*>y$TZaT9Mc~wQafN;XB;8 zcJr{M_YNHqaitVFL|ofV4iVRGlS9O{+2jzhWoki4aBDr1*sfX&S|!+3Rtb8oNNkx} z&fwTGwIC#%U1f04s~|zI6^SiV%W;VuhjdL&gV9@F%efvq5qYw5^-%eIYeB$O%4&)W|Kq2mZ=3H!L9X3V$0Nm zRta{MRf1kC5?iL0GdQ+PEeHu`R~a1iDoD_4MPkd;a$I7|)Pj(pR~a1iDoD_4MPkd; za$I7|)Pj(pR~a1iDoD_4MPkd;a$I7|)Pj(pR~a1iDoD_4MPkd;a$I7|)Pj(pR~a1i zDoD_4MPkd;w&jxjceqV$f4BI|%QE8Rq-Dys;MkTa+Y%E_u{t|AR+->fVbU^X zTkuKClx>L#j@8-0vC0I;3X_&8+k#J8rff@0aIDS_j#VZ&R+zL**%o}#GG$w0f@5`d zaI7-HvBIQf%C_K>mMPm36CA6vgJYEmjuj?dQ?`fiaVy*P;xjR4b1U0Xu_Gk5(jo_m zZPR%jBsaSso@DU!&mUiB;rB29^63MYA3y!}^ZU2&9zVSO>Bq}A7q<(m#J1mXTUmH1M&*I}~8GTCoju&b<-jT?5Ab+U2AuCh)x?$|Zfi7#b)4aYv*UVS-- z*bXPYk}U_xrVYE!D%rGP*Fmyzzpk<8_%gQFkjZ|x#-8KL*j|UpD(hs^hFxi$pxCG9 zR!;wT_IlHbU1ir{n51#FwzWj!)KDC%%O3b(pNN@7!+M zv8$Y7Z#S*jb&zb@u&W^P^=q#olb0fqqx8 ztj-RORVFxAnD`R59H00Swjd@rR%ZvtDia(lOnebrj!%3MTM!c*tFwb+l?jd&Ci21$ zuXkPeGPWGY9&oJA4vtkOI98bWGPWF__%gO2COB4S2gfQC94kzGCEK=qvM;&X>1*gq z{n@ks25|SV^rg-zWg8)}l@>WjY@1FF65F1WgT%Jw}#01Cc?BG~sf@6h=FJ;T|i7#af zVuE9Jc5tjR!Lh=`m$K#f#Fw%KF~PAqJ2+OE;8!I_a)`LLn;asxiY*8UZap4KCU3E0 zYuJKT33ioLf?g{UTfmkxIJSN*2nlCb865N~NYHCVVr$oOTw+Vtf{>tB865N~NYHCV zV$0TYTw<%%f{>tB865N~NYHCVVk_2iTw)8>f{>tB865N~NYHCVVvE&sTw-h0f{>tB z865N~NYHCVV(Zkl%l%BGtyp^%ne2CK>^Zh%?Uk6UvQ9Q_*p=1^ihVl1oIc}ty=ld+vg_EQ zwIG|=sOq^XV}B06}ys8 zHtyIpPA9f_?Uj78#=c{#*ItRqD(hs^j$P#xd%J1Hu7hOLhFt}TEnj;TnYM0`UJ-c6JN%bGdsSFEr}aY(Y$Ltj-RORVFxAnD|PzZTX~*#~LZy-TjtcK_3xug%&wPTw6{K zk-C0rqWIh=9dn4dwwoLxwu&tX32r?eN+$Ob*c!H=Rf1h*m7v#(#1^pS434c|3qr!# zRR#yW3KH~Mk=WX`9GBSAwIC$uRR#yW3KH~Mk=U}e9GBRtwIC$uRR#yW3KH~Mk=Tm0 z9GBRFwIC$uRR#yW3KH~Mk=SCj9GBQywIC$uRR#yW3KH~Mk=Q!5ZMo#GT*0zNrnbLZ zeBJPzt`{32akUgdNL(9E5E9ph6NJRI;RGSEwQ5OB5bW__vQn*6I;3#g?cHWrAaciLGA?@`){AOJag!b#`#9GQqLJ#1^py`NUSSB{9LVIy*R4nc!GqVk_A; z=99Vt+dENV%v0bkl6N|93-|aCkKfyWeZ}0VC87yOWA_f z35wNqf@6h=FJ;S_9bd{8#Dr6<&JK=MCOB4@_)@kUpZHR?ASO6gX9veB6C5i{d?{Ow zPkbp`5EC4$vx8%m362#ezLYJ;C%%*|hzX9>*}<{O1jh;!U&@x_6JN>}#01Cc?BG~s zf@6h=FJ;@3PY$xRta2;c^@dWmF%ny8k%Ppx>Es}>?KwF}Y+Fta5?{&|#00_0(ZrXs z1+5bltLp^E3KL(-mNPrPlr4w}r&ygG9IH%ltT6GVY&kyhrEEb=aIDS_j#VZ&R+#uw zwj7`MQnnx_I96u|$0`#XD@=STTaHhBDO(T|9ILZ~W0eVx6(-Ufy!hRja(v=T*@Bqh zSe+dlt4wgLF!7~q+w#f&JKV~4zZh*?U%t86FE@ZU&-=8)pP%-_(@**7pFh5SlfQrY zmrozK{P^j&pWnZI_xR!MPd^_2SGTu^+Yu30Xpuw2wdLdxacwv`L|ofV4v|G6?vFBB zy2?Ej;&t{Lc-Zq1_!+g67*V;*xI!m zm)O#^ASCEj1_!+g67*V;*s`@8m)NScASCEj1_!+g67*V;*ow6rm)L@}ASCEj1_!+g z67*V;*kZLDm)KghASCEj1_!+g67*V;*gCasxunJcr!SK)brR2QDIOMIHaw?HZA7Gg z?4`YU4iVROlS9O{+vE^&Z8kYXY?)dR65LvkB(_W~Xq8}BStaPTB3V3dHaxkaS6L;T zT?Gkx6(s1jBC%y^Ip-2vrWS+*y~^OAS3!baD-v6#mg5pzrWS+*y~^OAS3!baD-v6# zmg5pzrWS+*y~^OAS3!baD-v6#mg5pzrWS+*y~^OAS3!baD-v6#wk?-jf4ykzNMw4O zOV3PgM5O0yeIp`0x0?}>p1aM6NYBk?M5Jk^HbjD3>;Bs`QyZ=l>?*4Sy;dYmGqus+ z*lttH`Ss?Cv#SgadKDz-wIXSnsg3m7G*cTQL9a46=v9!Q*NUWRrZ#d((@brM1ii}O zpjSbHUMrHOncB!DO*6G267(vAgI)y*daX#BW@;msG|kk8NYJYc4tf;Yle_Qm#-Kq+nwA!$Rnm0R86j!9=Zugv-Eu}qx~6PHCJ0uJ zCS6mu;W|OFx=wJcFzK4Ijb^87$~I)eDOP6($0`#XD@?kkY$Km^P1%M_aIDS_j#VZ& zR+w~6*+xF;nz9X<;8>j<9IH%ltT5@CvW`?`Jl%h74O`GE!LG7O&}&6v3)pf7$JVa}A>r&QgM(fL33{zaZ0%Z(OKj;{ z5EAq%gM(fL33{zaY}s0lOKjCz5EAq%gM(fL33{zaY{goROKibf5EAq%gM(fL33{za zY_VF7OKh!L5EAq%gM(fL33{za7E`MI<(rG8_rSb5e$KXB^6(vQT)W@YdxwsQxKfH7 zBChQwhlp#p$syv}Y;uU$GPNKixV0WhY?)fnD#5O@O3-UXV$0NW2FI4E1tH<=DuaVw z1qphsNNkx}j!SHrS`ZTSDuaVw1qphsNNkx}j!SHrS`ZTSDuaVw1qphsNNkx}j!SHr zS`ZTSDuaVw1qphsNNkx}j!SHrS`ZTSDuaVw1qphsNNkzfrd%Q~=$+o?vJ~Y#H{82l zeB$MdZc`f)v85C-L~Pql3=!LI6GOzd*~Ad>WokJ{aBDr1_%gMeRf1h*m7v#(M9y$8 zy}vW)pN3XU_EeHvE6_=pbio}+wy$TZaT9Mc?wQaei#sRC`rgpz{FWWg~Y9k``V=um?BZi1;ySa}c;@WL; zh`2VJ93r+%EeHv2tw$1DrWUkHu&b;R^jeYFGPRt+v1MvONI1L7;GkDQf?g{UTc(!d z5?iJggap0H;GkDQf?g{UTc(!d5?iJggap0H;GkDQf?g{UTc(!d5?iJggap0H;GkDQ zf?g{UTc(!d5?iJggap0H;GkDQf?g{UTc)-lm)z~{Y8jcLN6I-wrG&{CpEr|)I zSe+dlt4wgLFtJr@K|Zl%Ye`IStj-RORVFxAnAp;_AfMRUwIn7uR%ZvtDia(lOl6I;o)DW6};-FyBi~Ml@>urT$@f164#y+grs`rr448h5?jia#00_0(ZrUrC9M+_ ztLp^E3KLt(7BoAylr4z~r&ygG9IH%ltT3^qY(YM;rEEz|aIDS_j#VZ&R+!jQwjiI_ zQnn-}I96u|$0`#XD@<%DTaZs|DO(Z~9ILZ~W0eVx6()-zK*OfMma+vo_7=zL?BG~s zf@6h=EoIx3PY(B08SA8M8@iQkjKo)3#2~3F=;e`83=-d-6NALJ<-{N{rffM(5Ud6JyF2;}c`bmcsg?cHWrAac zi7{o1@rf~I%VC0Jb#`#9GQqLJ#F(uYV@*Z05U_Ee1)`(qgnqnr=EHBu)365t61`&In1@ zlx@fa!9H!plWn+b$~IglC|1`Ajuj?dQ?}9UbWPcYOgP2r?BG~sf@6hA*OYDKlddV- zkO_{}*}<{O1jh=Kt|{BdCtXvvArl;{vx8%m362#eT~oG^Pr9aTLnb&@X9veB6C5i{ zx~6O+pL9*xhD>m*&JK=MCOB4@bWPdncyKG*{Rm0Z(qe?9>83M6Qa{$@qPqHRpSq;! zmNP=qHDwzzL9lW(>6)?)*Gc^#ioMw5Sec!!DO(+n)2+B`$~I)eDOP8v0>iNvS2$MI zN!OIEj>oBEyQXYICOB4Srvk&V7uPse)=Aftt&Yd3W4oqoLnb&@XQu+gu@^TuR@O<^ zl&y}(sbjmQY(pkER%fRI!?70!94qUjYsyx~Asn(jFxBu%%R5t6Pc+mH!@m7_`5 zlx?_9P^_*K94kz^rfj3x>6)?)nQ)5L*}<{O1jh=Kt|{BdCtXvvArl;{vx8%m362#e zT~oG^Pr9aTLnb&@X9veB6C5i{x~6O+pL9*xhD>m*&JK=MCOB4@bWPbtKIxjW4VmCr zogEyjOmM6)>6)_DrEr=HH+_TA5t62*#Ry5$O=pCp>7Fw}Qa{$DD6il4sb0ILY(pjp zR*oiJQ?}tcL9x0{Dli;-(KThOOW}0i>6)?)nQ)2~pIqTsWl|Rrj=kubvel(<>e#L+ z+mH#46`x$=SY=Wd5stm+nzGfUaO&8uDcg_QXp$Y}b@+$OOlVPi}FnGO3FQ$6j5Pyx-E&4rnr=BGBwbUsArk~E$F;60+i;zrSY0PLR+w~6*+#R|HDwzz;S{U0 zgJYEmjuj?dQ?`*$x~6PHCOB4S2gfQC94kz^rfeghbWPcYOmM8u4vtkOI98Z+P1#01 z>6)?)nc!HR9UQAnaI7%tnzD_2(luopGQqJrJ2+OE;8vY>jcH>I>E8Rq-n}FoSmjA+n5Qb zSe+dlt4wgLFlm~y4SmuyWg9cWu{t|AR+->fVbU~Z8~UVa$~I<#V|8|LtTMr|!lY@+ zHuOo;lx@re$Lj3hSY?7^g-O$tZRnGxDchI{j@8-0vC0I;3X`rW+n!t5%B^e-T|r0JG3Lee#58!|z#ay03hvJKY>iq&<3V}(iAlx;LST~oFp6Hc)@ zJ2+OE;8bHHmN}6suBP31toDq_yTh0hc*OYC@q<#>=UUW^_>QXq}boMA#j<96)?)nc!HR9UQAnaI7%tnzCJUD_gyl zZG@!iw@GO+Leg~886j!9=Zugv-Eu}qx~6PHCJ0uJCS6mu;W|OFx=wJcFzK4Ijb^87 z$~I)eDOP6($0`#XD@?kkY$Km^P1%M_aIDS_j#VZ&R+w~6*+xF;nz9X<;8>j<9IH%l ztT5@CvW6qsxnT0-RYXL4Vlyr;@FF89IHOTvBIQl%2t(e z>e#L++mK29AdbDb!LjNS94kz^rfgLir;hEKvJIKk58~L11CCXn;8&rJ6``vEozj*V!PdohiX+J#u zn4kXn<7<@n`&CgFj+dbKlf;VxAy$TZaT9MeYwH%k&sy$TZa zT9Md_wH%k&g0&zd=v4*>y$TZaT9MddwH%k&TD2e~=v4*>y$TZaT9Mc~we7fMf4$$` z)jHtxb#hmyw!dGzOYK}jwM8bC*Ngx3F^Gw4&q-P*u1zP2iEGzMVqz=SqM%^enM%=jSM87PQWq1Y<6B&XOqwkRlwc4mAK?FlrcWy7P>fblrGHOxosdLnv5Q zk0`!38r@iFJzObxc4nm@T7}X!cN>jQ+uUskh4bvp_#oODD2P^}w9VZ{PHCIF4WS^~ znejoiGf)t%LTQ`3jhxapcN;=Mv@_#_XlI}xT7}X!cN;mSZSFRNf@o*P2hq+zL9_~` zZSFR5O55CR2nEs3j1Qunfr4liO55D6YGaMt-L7`qy4`KW#1&#BF{!*>dgEOZ6W7j@ z#Kg7nBr&n&Zc$LMtR7Kpxm(mq!Lu_f1<@)LTke)LKDOK~3JT}hnejoiGf)t%Lb2s; zNlvllZc$JW?acTf+8HQ_R-xE(wUM5|D2xm%J`Y`I$$6hu2S zK8SV(3Zhjgw%jerDYo1#3JRi~86QMD0|n746kG1LHK!cD$L(&1MZ9PAW~L)1t`K7% z#Kg7rBr$RAJV{Jk8&47wTkaMG1vvr-VPLb2s;N#kS7-J+myo}C#V zL^}fo(JB;M?v~^fTkaMG1<}ro52Brcf@l?rEq6))ovpst_~vziEHBtLQ+>y6~%L95`@IH=>#FMm2OE)5bV?8 zjcH>I>E8R#1^>)&5o^cOJc$)R%ZvtDia(lOl*BykWXxRTM`o-tFwb+ zl?jd&CbqOK$S1b4Er|(^)!D(Z$^^#>6I<05g?cHWrAac$zlo6@TE~( z&lcp^TO6yigJYEmjuj@hmThA`xhr|z>1*gq>E3gd~8))6co<0GvkA3XP_Wj zg<>n)lAK~o+oGT#+L`e|v@=i;twOQoZAnhC^=(m55bezPAlexyh*qK48n+~;*dn(m zD2R4ud=Tvn6hx~~7Bhj4#euDKOA@Uub&G<6XlI;)XcdaBcH7$c)Wxty?sj*-6waMh z?KVQ<>M(+kxK8*%NL<@a5Ry7nFTKnz2#Kw9OJagxpAIMM+}xJ5PEf3_6C5i{Y>`{g z?ARK&Bqp3w{`|uvc>N>%( z!env9Klg6|gB)v1*^-!Wiq+Y{vC0I;3KLt(7UUCK%9g|g$Lj3hSY?7^g^4X?3-XCA zWlLg$V|8|LtTMr|!o-%c1^L95vL!LWu{t|AR+->fVPZ?!f_!32*^-#xSe+dlt4wgL zFtMd<+wzIrm$t^OZ1;=5B%D*qHbmkoErO7^Hk}|Ou01CRiEGOVLSjqVl9(V^Ihxo~ zwxo4}Vs)M1SYcvI*@9-rma-)=;S{U0gJYEmjuj@hlr6|7wv;W2369m-!LiB&#|jf$ z$`<4kTgsNi1jp*^;8pAIN1z0YXWO2M-;D+SRi6kF7mG(NVfEeZiuzQ*4o26cj`|Gd_rR1`488 zD7Mfo$tkwdEeZ;vof#iQI|BvLDimAowl$~Juwa#&+ZLagIjfu7Mo8+%{`ceC|9<@W z@z;0n-~Rpa!{aZ{{^On;B(9C;J_m_w+X+JA+H``D*h;q~CJ6TFaB_LN4f}3x%Q39& zO-Pew2gl0n*dn)}bz*DWl9+Ic)!E?~t4wgLFtPP*K|ZnNZAnaUtj-RORVFxAm@NM0 z*71o)o-5FGf@75lj#VZ&R+!kTwxBbcEow_*f@5`daI7-HvBJa_v<3OZ*0Uus!Ld3! zI98e9SYcvo**4~r>+f)L+k?EXXDz_aDrFlXaXotygv7P!1R-(lIYCHVTTT!XTgsNi z1i{MD#FnxptrHZh>jcLN6I;p_G&{DGEr|)ISe+dlt4wgLFtMdfVPZ?!f_!32 z*^-#xSe+dlt4wgLFtMd<8}mt3#_4r1OYa=|+`j}oJS=^svr5@UNL-~w5R!_kW{M40 ziEGaZS|zS6CkTlxWlLg$VC86HOWBgv35wNqf@6h=EoBRu9b3wl#Dr6<&JK=MCOB4@ z*iyD2pV(5iBqlgkX9veB6C5i{Y$;oiPi!e$5)&M&vx8%m362#ewv;W%C$^L=i3yI? z*}<{O1jh;!Tgn#X6I;rb#01Cc?BG~sf@6h=EoIx3Pp+@(QdlEpyW3LAHb&wqErO7^ zHk}|Ou01CRN%hQ2gU28wwv;W234)cQi7jPIS|=!0*9ndlCbpCfVPZ?!f_!32*^-#xSe+dlt4wgLFtMd6I;p_g?cHWrAaci7jOd@`){FOJag!b#`#9GQqLJ#FnxJ z`NWp8B{9LVIy*R4nc!GqVoTYAd}2%4l9=FFogEyjOmM6)v88O=@`*eRy~eF<``zLW zc1G_odOc*~N-dI@xVD`nCa!HKiHU35Nn&E_*`lCe*{1`_#pUU4Y%AKLRtlb-St*EC zq1d9fr17y;ZBbA-&(4ewqMd<)Xcdb5`ux%hn3J4hOWUHLAljMnL9{ba5UoP75`H=p}W-Q0FPLgMN$f{?g2 zo**QyZ6^qcYtsorVk_N}m>}4v!^t|GThcl~vARxhtT3@fZb7qSYuu8UaEjI0!LiB& z#|jf$-xlN(Ti%w$1jp*^;8)Dc+;8>j<9IH%ltT3^)Y#Z}QO$XLU+4fhvr7G^MZf+Yf zsUNG|_;d5VBqpwHCy9w`+eu>L+IEtd*m|}oC|LICfU;827PV6F?956*v(=g2H)rW_%Fs3=~AGP<**tkW+lQTM`sRJ2O6rb_NQf zRVcpPEyyXp+${+TqMaEZL^}fo(JB;Q?iS<}U+$I!1<}ro52Brcf@l?rFLw)aiZ6Fd zf`Vvg#s|^PKtZ$$#h1Hn%qdkHYuxU3zxYz)S-reILgMN$f{;{PFTL+B2#IUk2}0u9 zbb^p9Mn}&r-IACf*r&tEN^ew}vk%)cx1`y@u`)Z0E54y)ZH-&d>|Ak*)!E?~t4wgL zFtPP*K|ZnNZAnaUtj-RORVFxAnAp;`AfMREwj?GvR%ZvtDia(lOl(zKkWXw;TM`o- ztFwb+l?jd&Cbpn0$S1a*Er|(^)!D(Z$^^#>6I;u+F`pd1!_93s51V?Q(h(9@X%U3P zwdn*QaqT%lNL*V^5E5I;mc#_X%F)DjWn0oZL9x0{aI7$~rEEd7V@uhRm~e{K*}<{O z1jh;!Tgn#X6I;rb#01Cc?BG~sf@6h=EoBSxi7jPIVuE9Jc5tjR!Lh=`ma+x;#Fnxp zF~PAqJ2+OE;8Ze`nTDrFlXag`SL zIY?ZaP7sp1f?j-4UCt_TZ8<@!#FnxpF+s3$G_j>@N$Ui~>N>%(!o-%c1KsKZf;9r(lf*uuamllUV2ek(mLt6^NiO?&y8oyq-pN9 z00qnH5yf_aTasq?cy?x`AX=)7)+Bl%~1c0u)3$Gd_rR1`488 zC{1&>u~VAnZVONl?acTf+8HQ_R-v@b-45U5cDL)rXJ*dmcDKX*h>0!42x4N}dV-kP zcAg+6wv8uXA6^bu+3v!At zcT0kTXlKR;(au0Yv==*2hQ1u?O0JV8u+xmywxEUQNpU+$K)Qt<4|NJi14yCtm@JUg>e z5UoP-XA6^bu+3v!At zcT0kTXlKR;(au0Yv+EVpdi|r@jw!!iL{PDH?{Qb+neEQ(! z$4|fg{Qm8`#}99R`tkD3#r}GKxF4+&*S7N-NFHvlzI^jOB9+!lFS~mUBKzGci^LYX zy#|wYR>^+bdR=FkY}v0XEt4(#buF80*|6&@6IZGZi`iZS$x1popjgq#VcUvbM<-iW z>^i3tTgUb~I$33z*fO@)V6xIO!LVZ3+ie?mmEVbPw`|yzPA9g4?KO1rVkYXA6^gBCOLB^>YKww`XlKR; z(au0Yvys95;4o9BJn;m=Qd(Zy5uHzOvt5F?0*ZR>d*CNHe<$Lqwl^SlO=m)7`WB({C$ zHITgcB7-p#U+wl9P}W%|wp-hter5|B${PF5Dn9YWZm$7lrG>I-$*yITO>1_Yh2l%y zUSoLv!XL8?PqG|k_4mN5jh|&-_~&usL(0|0J-ZH+O?!5w&BvFzy@qJl*?fGZ+iNgc zYoTn~vnwqWG^-o3?FzS7`eksHy=U>)+B1Jp0(ZM z_DU;d6{Yw(w;ZMTGPl=YveI)YwyWG;X?j*UrP%IrdnG9AC}q=68?19m@l|fGqm)$^ zim!5e4Ja!ulubWtu+rXRyU^{GG`q^)W4qBU35qXrdkv-hh4HDcbe-NJvs4H?_oj<9IH%l ztT3^)Y#Z~*b-B=Hjg)PFxA=-&WsPDoq>XA6^gBKOLB@Wa*Kk3XlKR;(au0YvOm4__T}lH++J^7v#abqzR)d5DZbF{HK44cl$%X!cBSL&4W3oc-fY~n zt32uE>)djb;>+A#gUL!tIpA4U%3QyN5aXA6^bu(3v!CDbW4JQXlKR;(au0Yv-9uWR3?8< z>B`;i7n_djr~8kVL&2NpecIvAPkYHl7{}Uegv8Zh1R-&4JV8iY+fEP?*QOJMWYLPx z&54tkAXwRkZJk@vIzh3zPH?O+u|;k{vtw)Al9+Ic)!D(Z$^^#>6Ig?cHWrAaci7jml@`}qTgsNSPEf3_6C5i{7Ehkf&4q(z$Ck1sG2s-evx8%m362#e zwv;W%C$^L=i3yI?*}<{O1jh;!Tgn#X6I;rb#01Cc?BG~sf@6h=EoBSxi7jPIVuE9J zc5tjR!Lh=`ma+x;#FnxpF~PAqJ2+OE;84t`i(9OuD9QquJ@2vJIJViq+Y{vC0I;3X`rW z+sG$fQ??-!9ILZ~W0eVx6((I%wvkV|rffqdI96u|$0`#XD@?kkY$Km^P1%M_aIDS_ zj#VZ&R+w~6*+xF;nz9X<;8>j<9IH%ltT5@BvK`;l^E;$$o4TlOjKo!11R-&4IzdQW zdrlA%*On86#FnxpF+s3$G_j>@N$Ui~>N>%(!o-%c1q~n3%QqMGqKx`ESI5tZf`Vvg#s|^PKtZ$$#TL3HImK4GML|Kd zGvkA3XP_Wjg<`ASw&s-U?{ITl5|geW#%P^%-Fik$y6!w9CS5n45tFvL+Ykzt)gwyV z+-itI6Hc)@J2+OE;8)V2SV$0i-nBZ8Q9UQAnaI7$~rENhzv6XE}OmM8u4vtkOI98b0s}q+m&re>jcH>I>E8R#FnxJ&5kW)OJc$)R%ZvtDia(l zOl&DzkWXwWTM`o-tFwb+l?jd&CbpC<$S1axEr|(^)!D(Z$^^#>6I;p_g?cHWrAaci7jOd@`){FOJag!b#`#9GQqLJ#Fnyc%qMj`*0`1Je({Nzv$~aSgv3@_ z@Moj#VZ&R+#uwwj7`MQnnx_I96u|$0`#XD@+zG@Z7(&4th$-m$EI=-OTgTH#k;j z2gfQC94kzGDO-+Dd?{NH6CA6vgJYEmjuj@plr6_6zLYJ9369m-!LiB&#|jf)%C;?^ z+j<9IH%ltT0)8Qn}%C z1-6te$g#GREr|(^RiEHkVPZ?!f@a5-vL!L$9ILZ~W0eVx6(+WnEyyRflr4z~j@8-0 zvC0I;3KLt(wlSa7rLfAaZ1=ko%++hE&MIXaA#s%!_c=&hn@$iC*PauEqz+Xzz;mC9 z4MJi|*^-zbSUH;5QnsXZf?{=@;8O#I@}tF>!4> zNldD1UYfHdF|qY*QBbh#(*fn;@^m-06>U)~1<%f`6hx~~Y*Aa%_}HqpC@7p~XT}H7 z&Okx53dL5oB{{{Gwnaffv@_#_XlI}xT7_cE+mf7O>)WEBAljMnL9{ba5UoP7HEu~x zu|;lCP!R3R_#oODD2P^}*h04?r`SrjC@6?_W_%Fs3=~AGP;9l^ww!W*TeWfe0{c>< z_T1Z+_PfP>?2N8%8#8f*7)ea(8hUBQl*Gif^CU5GZ9GX#Y`I$$6fCPp6kF~VwNmix z%t}GD3dNSYC5?|QcZ-6;d3I)e5bX>UM5|D2xm%J`Y`I$$6hu2SK8SV(3Zhjgw%jer zDYo1#3JRi~86QMD0|n746kG0=-&WsPDoq>XA70O~J(6KnMc;=6Jzcc z1O?0L5yhCh1+5f3JF`*{twNEL?#q8c&KVzL?iK`v^X$y{Alexyh*qH(bGIC)7<0EE zD2R4ud=Tvn6hx~~jJaEmQ;fM=5EMi^Gd_rR1`488D8}3^$0^3#EeHytof#iQI|BvL zDimYxmg5v-?iK_E(awwyqMd<)XcdYvciWaz_W$w2XBvP0pUK;&`EGa1VbV3k7_F18 zThE9|*PUm?r0d2rV$wEu8$!XddPHfPyA4+go}F1Kh*qJr&D}=h(>8Y-Lg73+Gd_rR z1`488C~b4MkyF~{ZbK-Dc4mAK?F6(&E1Aj5bezPAlexyh*qJr&D};$ zX`8zZp&;6s@j6t6>4RyY1yQz*8ob zSC>0N>i2!hr0do*V$yZz88PX)@r;Ibpxn~S!&TU`vNThAWP>S1<|XcbD^ z+-)>Ia@0k%Z!WGl&(4ewqMd<)XcbD^+-)S6(&E1Aj5bezPAlexy zh*qJr&E2lK-RB|jm&c~y`uJw1BPP|sU0xp?G3mPXjF@!Yc}7gSZagC< zZF9FF6fCPpl(xCsaHZhcnU#WQ6-wLOZ8Sb@bGIQB&a*S)gJ@@i#s|^PKtZ$$rETsua!T9WZ3qR?&WsPDoq>XA6-wLOZRC`;x!Vv5qMaEZL^}fo z(JGX-x!cGoZF9FF6hu2SK8SV(3ZhjgZF9G(jnk`By1tp|h)LHFW5lF>tV;q|zwgu8 z>ALfbm~`EEMoik~ZbK+oR*xucbGPA2!Lu_f1<@*$wz=DAeA?!2Ln!rwoM(?$-XYo< zryyE|(l&Ri+Bp4nux;)(gi=3=+uUvBl(xCs5DKE586QMD0|n74l(xCs z$SG}ew;>cnJ2O6rb_NQfRVZz9w~UM5|ER=58aWw9VayP!R3R z_#oODD2P^}w9Vb>gq(K8U2k_AG3gp&jF@!YdPYp@$GX(#_4_{cN!N{M#H4NRHiUv@ z^@!3ocN?yh`awKlxG zqHUYI4WS^~nenN_5bg2GJ4CB1rETt3C*)MLZF9FF6hu2SK9v}vJzjZ-XmzEu&E4vR zoQk$>?ly#iXlKT!5<|4dEAJ4ku9UX9Tb+-EG9AYltyo(sk<@G3mPVjF@!Yct%Xx=59kMSXPfHZF9HbO2M-;D+SRil(xCs zXnfk{ZbK-XXJ^I-(au0Yv=+uUvBl(xCs5DKE5 z86QMD0|n74l(xCs$SG}ew;>cnJ2O6rb_NQfRVZz9w~UM5|ER z=5BQ{oOZ=sZ+9Cp=^A2;m~`EGMohZyJR>IcV_o9&`hA}|wr%bQ zJF`+MF+_X3iHXy!u9UX9Tb+>8E%}P`?956*v@=lZGD5V+o0t%-u9UX9Tb+XA6-w9KZRnJ)x!V{DqMaEZL^}fo z(JGXtx!d(!osiS#Xl=RM_2MrK*OzZD_PgEEfAQvdpLY23(|-8;gFgPBKfc~!_xqQB z`SiidkDq=kx4Vs**g}jTCbq37h)JER7r*8c#KgAo1TpdDZb?wE?B|{9@^m-$XA z6^bu+3p%{{aUM5|C1U%lENUvFA^>FVm~HK`z{_;R--D2R4ud=Tvn z6hx~~e7ReYQ+&Bw5)?!`Gd_rR1`488D8AfnV@|pI8iGAhnY_Thbb^2GDgSQq8St~J z-9|`U&sql|acw+7NL<@a5E9p>6NJQ8x+O6|u(A!?I=7^Cf?{=@;8)Dc+;8>j<9IH%ltT3^)Y_IQ=&sUTE z*FO`r%lc>2Pj{dF=a@EopFj4m>)GZW%k7_!<;yqv@2_q<{hn(_@%g_K{ykq;LALm& zs{uQF)Ae-F;d}15xI3x~SvZSkR+%5!r3W znQqmi+|i?ac@v-GRA11ebbQI49;L~v*Buvk$32`LrOOeO9;MAsx}(VGQMxQ|dXz3d zX?m14Kk1e)1a$l*i}&m4GSl3T(&Z=3{U}|2(%g^Ig@bCryvi<|p0p zDd6ZH<(K!r{^RXWzy3de8+9l@et7@!<3IlX_T&Hj?eE9W+m|=Fc>LeTPmdqo{-4Kp z|MNKW;djT6{rMIC%}4*=_rLwm-+un_{fC*4|MLFR`wws5{o~`Ozy0#l|M!08Gamo( z;c@s0e}DY@`w#DqkAL`8{`pOeKIQoBeR})x)8jAWuk_1rpMLuE^ZVg{?f6wbe0uvC z{^-B>>*FuSuk$~Tzy6=k-}skO=8wsHF8%%ZwLZT6`KOPcj&%P0-%!w 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": [], + }