diff --git a/README.md b/README.md index 150ec30..7ce7af5 100644 --- a/README.md +++ b/README.md @@ -301,7 +301,32 @@ To enable support for multiple users perform the steps below. zoffline's previou +### Step 7 [OPTIONAL]: Install Zwift Companion App + +Create a ``server-ip.txt`` file in the ``storage`` directory containing the IP address of the PC running zoffline. + +
Android (non-rooted device) + +* Install apk-mitm (https://github.com/shroudedcode/apk-mitm) +* Copy the file ``ssl/cert-zwift-com.pem`` in this repo and the Zwift Companion apk (e.g. ``zca.apk``) to a known location +* Open Command Prompt, cd to that location and run + * ``apk-mitm --certificate cert-zwift-com.pem zca.apk`` +* Copy ``zca-patched.apk`` to your phone and install it +* Download "#1 HOST CHANGER - BEST FOR GAMING" from Google Play ([link](https://play.google.com/store/apps/details?id=best.see.world.company)) +* Create a ``hosts.txt`` file to use with the app (you could use a text editor app or create it online with an online tool such as [this](https://passwordsgenerator.net/text-editor/)). The file must look like this (replace ```` with the IP address of the machine running zoffline): +``` + us-or-rly101.zwift.com + secure.zwift.com +``` +* Run "Host Changer", select created ``hosts.txt`` file and press the button +* Optionally, instead of using the "Host Changer" app, you can create a ``fake-dns.txt`` file in the ``storage`` directory and set the "DNS 1" of your phone Wi-Fi connection to the IP address of the PC running zoffline + * If running from source, install the required module with ``pip3 install dnspython`` +* Note: If you know what you're doing and have a capable enough router you can adjust your router to alter these DNS records instead of using the "Host Changer" app or changing your phone DNS. + +
+ ### Extra optional steps +
Expand * To obtain the official map schedule and update files from Zwift server: create a ``cdn-proxy.txt`` file in the ``storage`` directory. This can only work if you are running zoffline on a different machine than the Zwift client. diff --git a/game_info.txt b/game_info.txt new file mode 100644 index 0000000..08b609a --- /dev/null +++ b/game_info.txt @@ -0,0 +1 @@ +{"maps":[{"name":"NEWYORK","routes":[{"name":"The Highline Reverse","id":1763213625,"distanceInMeters":10528.8,"distanceInMetersFromEventStart":0.0,"ascentInMeters":178.8,"locKey":"LOC_ROUTE_NEWYORK_THE_HIGHLINE","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/NEWYORK_The_Highline_Reverse.png","levelLocked":1,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":203.6,"leadinDistanceInMeters":10482.1,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Grand Central Circuit","id":2945813240,"distanceInMeters":6857.7,"distanceInMetersFromEventStart":0.0,"ascentInMeters":144.2,"locKey":"LOC_ROUTE_NEWYORK_GRAND_CENTRAL_CIRCUIT","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/NEWYORK_Grand_Central_Circuit.png","levelLocked":1,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":18.6,"leadinDistanceInMeters":1596.8,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Gotham Grind Reverse","id":352245150,"distanceInMeters":9259.2,"distanceInMetersFromEventStart":0.0,"ascentInMeters":96.1,"locKey":"LOC_ROUTE_NEWYORK_GOTHAM_GRIND","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/NEWYORK_Gotham_Grind_Reverse.png","levelLocked":1,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":2.1,"leadinDistanceInMeters":390.5,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"LaGuardia Loop Reverse","id":3774003351,"distanceInMeters":2791.3,"distanceInMetersFromEventStart":0.0,"ascentInMeters":27.2,"locKey":"LOC_ROUTE_NEWYORK_LAGUARDIA_LOOP","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/NEWYORK_LaGuardia_Loop_Reverse.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":24.6,"leadinDistanceInMeters":2535.1,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Couch To Sky K","id":1790569309,"distanceInMeters":6750.3,"distanceInMetersFromEventStart":0.0,"ascentInMeters":67.7,"locKey":"LOC_ROUTE_NEWYORK_SKY_K","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/NEWYORK_Couch_To_Sky_K.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.1,"leadinDistanceInMeters":139.6,"blockedForMeetups":0,"sports":["RUNNING"]},{"name":"Lady Liberty","id":5103974,"distanceInMeters":12361.1,"distanceInMetersFromEventStart":0.0,"ascentInMeters":206.0,"locKey":"LOC_ROUTE_NEWYORK_LADY_LIBERTY","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/NEWYORK_Lady_Liberty.png","levelLocked":1,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.0,"leadinDistanceInMeters":369.5,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Gotham Grind","id":480315274,"distanceInMeters":9256.5,"distanceInMetersFromEventStart":0.0,"ascentInMeters":96.3,"locKey":"LOC_ROUTE_NEWYORK_GOTHAM_GRIND","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/NEWYORK_Gotham_Grind.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":0.0,"leadinDistanceInMeters":258.4,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Hudson Roll","id":3665959404,"distanceInMeters":9020.8,"distanceInMetersFromEventStart":0.0,"ascentInMeters":80.2,"locKey":"LOC_ROUTE_NEWYORK_HUDSON_ROLL","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/NEWYORK_Hudson_Roll.png","levelLocked":1,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":1.2,"leadinDistanceInMeters":174.7,"blockedForMeetups":0,"sports":["RUNNING"]},{"name":"Park Perimeter Loop","id":1919980508,"distanceInMeters":9762.2,"distanceInMetersFromEventStart":0.0,"ascentInMeters":126.1,"locKey":"LOC_ROUTE_NEWYORK_PARK_PERIMETER_LOOP","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/NEWYORK_Park_Perimeter_Loop.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":2.5,"leadinDistanceInMeters":406.0,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Shuman Trail Reverse","id":274775181,"distanceInMeters":2534.8,"distanceInMetersFromEventStart":0.0,"ascentInMeters":9.0,"locKey":"LOC_ROUTE_NEWYORK_SHUMAN_TRAIL","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/NEWYORK_Shuman_Trail_Reverse.png","levelLocked":1,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":0.8,"leadinDistanceInMeters":109.8,"blockedForMeetups":0,"sports":["RUNNING"]},{"name":"Park Perimeter Reverse","id":2202609830,"distanceInMeters":9762.0,"distanceInMetersFromEventStart":0.0,"ascentInMeters":126.1,"locKey":"LOC_ROUTE_NEWYORK_PARK_PERIMETER","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/NEWYORK_Park_Perimeter_Reverse.png","levelLocked":1,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":0.0,"leadinDistanceInMeters":243.1,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"NYC KOM After Party","id":2372883204,"distanceInMeters":36626.3,"distanceInMetersFromEventStart":0.0,"ascentInMeters":474.7,"locKey":"LOC_ROUTE_NEWYORK_NYC_KOM_AFTER_PARTY","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/NEWYORK_NYC_KOM_After_Party.png","levelLocked":1,"publicEventsOnly":true,"supportedLaps":false,"leadinAscentInMeters":1.7,"leadinDistanceInMeters":377.3,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Rising Empire","id":3078665969,"distanceInMeters":20816.0,"distanceInMetersFromEventStart":0.0,"ascentInMeters":376.9,"locKey":"LOC_ROUTE_NEWYORK_RISING_EMPIRE","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/NEWYORK_Rising_Empire.png","levelLocked":1,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":1.8,"leadinDistanceInMeters":380.4,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"LaGuardia Loop","id":2422779354,"distanceInMeters":2789.3,"distanceInMetersFromEventStart":0.0,"ascentInMeters":27.3,"locKey":"LOC_ROUTE_NEWYORK_LAGUARDIA_LOOP","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/NEWYORK_LaGuardia_Loop.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":18.6,"leadinDistanceInMeters":1568.4,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Shuman Trail Loop","id":2590569306,"distanceInMeters":2534.8,"distanceInMetersFromEventStart":0.0,"ascentInMeters":9.0,"locKey":"LOC_ROUTE_NEWYORK_SHUMAN_TRAIL_LOOP","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/NEWYORK_Shuman_Trail_Loop.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":1.6,"leadinDistanceInMeters":312.2,"blockedForMeetups":0,"sports":["RUNNING"]},{"name":"Astoria Line 8","id":1509089537,"distanceInMeters":11569.0,"distanceInMetersFromEventStart":0.0,"ascentInMeters":141.4,"locKey":"LOC_ROUTE_NEWYORK_ASTORIA_LINE_8","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/NEWYORK_Astoria_Line_8.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":2.7,"leadinDistanceInMeters":420.4,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Grand Central Circuit Reverse","id":3687251774,"distanceInMeters":6856.7,"distanceInMetersFromEventStart":0.0,"ascentInMeters":143.9,"locKey":"LOC_ROUTE_NEWYORK_GRAND_CENTRAL_CIRCUIT","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/NEWYORK_Grand_Central_Circuit_Reverse.png","levelLocked":1,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":23.0,"leadinDistanceInMeters":2499.7,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Park To Peak","id":1378559127,"distanceInMeters":4576.3,"distanceInMetersFromEventStart":0.0,"ascentInMeters":125.9,"locKey":"LOC_ROUTE_NEWYORK_PARK_TO_PEAK","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/NEWYORK_Park_To_Peak.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":false,"leadinAscentInMeters":0.1,"leadinDistanceInMeters":139.6,"blockedForMeetups":0,"sports":["RUNNING"]},{"name":"Knickerbocker Reverse","id":2001106885,"distanceInMeters":22544.8,"distanceInMetersFromEventStart":0.0,"ascentInMeters":364.7,"locKey":"LOC_ROUTE_NEWYORK_KNICKERBOCKER","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/NEWYORK_Knickerbocker_Reverse.png","levelLocked":1,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.0,"leadinDistanceInMeters":157.4,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"2022 Cycling Esports World Championships Route","id":2436095601,"distanceInMeters":54743.4,"distanceInMetersFromEventStart":0.0,"ascentInMeters":942.4,"locKey":"LOC_ROUTE_NEWYORK_UCIESPORTS22","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/NEWYORK_2022_Cycling_Esports_World_Championships_Route.png","levelLocked":1,"publicEventsOnly":true,"supportedLaps":false,"leadinAscentInMeters":0.3,"leadinDistanceInMeters":219.9,"blockedForMeetups":1,"sports":["CYCLING"]},{"name":"The 6 Train Reverse","id":274639515,"distanceInMeters":6468.7,"distanceInMetersFromEventStart":0.0,"ascentInMeters":68.9,"locKey":"LOC_ROUTE_NEWYORK_THE_6_TRAIN","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/NEWYORK_The_6_Train_Reverse.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":1.6,"leadinDistanceInMeters":374.3,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"The 6 Train","id":3597939700,"distanceInMeters":6467.3,"distanceInMetersFromEventStart":0.0,"ascentInMeters":69.0,"locKey":"LOC_ROUTE_NEWYORK_THE_6_TRAIN","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/NEWYORK_The_6_Train.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.0,"leadinDistanceInMeters":231.5,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Flat Irons","id":711818913,"distanceInMeters":14850.2,"distanceInMetersFromEventStart":0.0,"ascentInMeters":146.3,"locKey":"LOC_ROUTE_NEWYORK_FLAT_IRONS","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/NEWYORK_Flat_Irons.png","levelLocked":1,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":2.4,"leadinDistanceInMeters":574.3,"blockedForMeetups":0,"sports":["RUNNING"]},{"name":"Mighty Metropolitan","id":3872978134,"distanceInMeters":20095.3,"distanceInMetersFromEventStart":0.0,"ascentInMeters":318.4,"locKey":"LOC_ROUTE_NEWYORK_MIGHTY_METROPOLITAN","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/NEWYORK_Mighty_Metropolitan.png","levelLocked":1,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":2.6,"leadinDistanceInMeters":411.8,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"The Highline","id":1732356505,"distanceInMeters":10526.4,"distanceInMetersFromEventStart":0.0,"ascentInMeters":178.7,"locKey":"LOC_ROUTE_NEWYORK_THE_HIGHLINE","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/NEWYORK_The_Highline.png","levelLocked":1,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":202.7,"leadinDistanceInMeters":10035.2,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Knickerbocker","id":2954366662,"distanceInMeters":22545.9,"distanceInMetersFromEventStart":0.0,"ascentInMeters":364.8,"locKey":"LOC_ROUTE_NEWYORK_KNICKERBOCKER","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/NEWYORK_Knickerbocker.png","levelLocked":1,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":2.2,"leadinDistanceInMeters":393.1,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Everything Bagel","id":1327665278,"distanceInMeters":34285.6,"distanceInMetersFromEventStart":0.0,"ascentInMeters":544.7,"locKey":"LOC_ROUTE_NEWYORK_EVERYTHING_BAGEL","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/NEWYORK_Everything_Bagel.png","levelLocked":1,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.0,"leadinDistanceInMeters":221.4,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]}]},{"name":"WATOPIA","routes":[{"name":"Big Loop","id":2947111049,"distanceInMeters":42603.8,"distanceInMetersFromEventStart":0.0,"ascentInMeters":661.7,"locKey":"LOC_ROUTE_WATOPIA_THE_BIG_LOOP","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Big_Loop.png","levelLocked":5,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":1.1,"leadinDistanceInMeters":521.5,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Out And Back Again","id":2748657713,"distanceInMeters":39896.7,"distanceInMetersFromEventStart":0.0,"ascentInMeters":327.9,"locKey":"LOC_ROUTE_WATOPIA_OUT_AND_BACK_AGAIN","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Out_And_Back_Again.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":6.2,"leadinDistanceInMeters":2358.4,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Italian Villas Circuit","id":3573087582,"distanceInMeters":1937.2,"distanceInMetersFromEventStart":0.0,"ascentInMeters":15.1,"locKey":"LOC_ROUTE_WATOPIA_ITALIAN_VILLAS_CIRCUIT","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Italian_Villas_Circuit.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":2.8,"leadinDistanceInMeters":777.3,"blockedForMeetups":1,"sports":["RUNNING"]},{"name":"11.1 Ocean Blvd","id":4240327959,"distanceInMeters":10469.4,"distanceInMetersFromEventStart":0.0,"ascentInMeters":47.6,"locKey":"LOC_ROUTE_WATOPIA_111_OCEAN_BLVD","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_11_1_Ocean_Blvd.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":false,"leadinAscentInMeters":2.6,"leadinDistanceInMeters":537.3,"blockedForMeetups":0,"sports":["RUNNING"]},{"name":"Four Horsemen","id":2878180967,"distanceInMeters":89788.8,"distanceInMetersFromEventStart":0.0,"ascentInMeters":2108.7,"locKey":"LOC_ROUTE_WATOPIA_FOUR_HORSEMEN","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Four_Horsemen.png","levelLocked":6,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":2.5,"leadinDistanceInMeters":595.1,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Jungle Circuit","id":743730361,"distanceInMeters":7862.3,"distanceInMetersFromEventStart":0.0,"ascentInMeters":82.7,"locKey":"LOC_ROUTE_WATOPIA_JUNGLE_CIRCUIT","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Jungle_Circuit.png","levelLocked":5,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":9.5,"leadinDistanceInMeters":5732.9,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Ocean Lava Cliffside Loop","id":1386460176,"distanceInMeters":19054.6,"distanceInMetersFromEventStart":0.0,"ascentInMeters":156.1,"locKey":"LOC_ROUTE_WATOPIA_OCEAN_LAVA_CLIFFSIDE_LOOP","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Ocean_Lava_Cliffside_Loop.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.1,"leadinDistanceInMeters":268.1,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Muir And The Mountain","id":2139465450,"distanceInMeters":34141.8,"distanceInMetersFromEventStart":0.0,"ascentInMeters":792.5,"locKey":"LOC_ROUTE_WATOPIA_MUIR_AND_THE_MOUNTAIN","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Muir_And_The_Mountain.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":122.0,"leadinDistanceInMeters":5152.3,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Three Sisters","id":4142772830,"distanceInMeters":48010.5,"distanceInMetersFromEventStart":0.0,"ascentInMeters":895.8,"locKey":"LOC_ROUTE_WATOPIA_3SISTERS","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Three_Sisters.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.4,"leadinDistanceInMeters":456.5,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Sand And Sequoias","id":604330868,"distanceInMeters":20150.3,"distanceInMetersFromEventStart":0.0,"ascentInMeters":174.8,"locKey":"LOC_ROUTE_WATOPIA_SAND_AND_SEQUOIAS","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Sand_And_Sequoias.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":6.2,"leadinDistanceInMeters":2374.1,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Watopia Figure 8","id":3382019812,"distanceInMeters":29695.4,"distanceInMetersFromEventStart":0.0,"ascentInMeters":254.0,"locKey":"LOC_ROUTE_WATOPIA_FIGURE8","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Watopia_Figure_8.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.7,"leadinDistanceInMeters":492.4,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Volcano Climb","id":849508252,"distanceInMeters":22861.3,"distanceInMetersFromEventStart":0.0,"ascentInMeters":203.1,"locKey":"LOC_ROUTE_WATOPIA_VOLCANO_CLIMB","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Volcano_Climb.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.6,"leadinDistanceInMeters":477.8,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Three Little Sisters","id":2627606248,"distanceInMeters":37736.7,"distanceInMetersFromEventStart":0.0,"ascentInMeters":434.6,"locKey":"LOC_ROUTE_WATOPIA_THREE_LITTLE_SISTERS","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Three_Little_Sisters.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":3.1,"leadinDistanceInMeters":689.5,"blockedForMeetups":1,"sports":["CYCLING"]},{"name":"Run Path Reverse","id":772562418,"distanceInMeters":4999.0,"distanceInMetersFromEventStart":0.0,"ascentInMeters":30.5,"locKey":"LOC_ROUTE_WATOPIA_RUN_PATH","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Run_Path_Reverse.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":1.9,"leadinDistanceInMeters":169.0,"blockedForMeetups":0,"sports":["RUNNING"]},{"name":"Watopia Pretzel","id":2494975884,"distanceInMeters":72535.8,"distanceInMetersFromEventStart":0.0,"ascentInMeters":1360.5,"locKey":"LOC_ROUTE_WATOPIA_PRETZEL","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Watopia_Pretzel.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.7,"leadinDistanceInMeters":485.2,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Watopia Flat Route","id":3395698268,"distanceInMeters":10268.9,"distanceInMetersFromEventStart":0.0,"ascentInMeters":61.2,"locKey":"LOC_ROUTE_WATOPIA_FLAT","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Watopia_Flat_Route.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.4,"leadinDistanceInMeters":456.5,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Road To Ruins Reverse","id":2967612381,"distanceInMeters":29645.6,"distanceInMetersFromEventStart":0.0,"ascentInMeters":275.0,"locKey":"LOC_ROUTE_WATOPIA_ASPHALT_JUNGLE","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Road_To_Ruins_Reverse.png","levelLocked":5,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":0.1,"leadinDistanceInMeters":200.5,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Seaside Sprint","id":3454338139,"distanceInMeters":6291.2,"distanceInMetersFromEventStart":0.0,"ascentInMeters":46.8,"locKey":"LOC_ROUTE_WATOPIA_SEASIDE_SPRINT","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Seaside_Sprint.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":36.5,"leadinDistanceInMeters":2961.9,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"May Field","id":3012588561,"distanceInMeters":400.3,"distanceInMetersFromEventStart":0.0,"ascentInMeters":0.0,"locKey":"LOC_ROUTE_WATOPIA_MAY_FIELD","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_May_Field.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.0,"leadinDistanceInMeters":48.7,"blockedForMeetups":0,"sports":["RUNNING"]},{"name":"Watopia Mountain Route","id":2966818006,"distanceInMeters":29723.1,"distanceInMetersFromEventStart":0.0,"ascentInMeters":682.5,"locKey":"LOC_ROUTE_WATOPIA_MOUNTAIN","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Watopia_Mountain_Route.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.5,"leadinDistanceInMeters":465.9,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Volcano Circuit CCW","id":3866241330,"distanceInMeters":4103.6,"distanceInMetersFromEventStart":0.0,"ascentInMeters":20.4,"locKey":"LOC_ROUTE_WATOPIA_VOLCANOCCW","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Volcano_Circuit_CCW.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":29.7,"leadinDistanceInMeters":4905.7,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Watopia Mountain 8","id":3219074012,"distanceInMeters":32255.9,"distanceInMetersFromEventStart":0.0,"ascentInMeters":691.4,"locKey":"LOC_ROUTE_WATOPIA_MOUNTAIN8","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Watopia_Mountain_8.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.7,"leadinDistanceInMeters":492.4,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Triple Flat Loops","id":3453194200,"distanceInMeters":33978.4,"distanceInMetersFromEventStart":0.0,"ascentInMeters":156.8,"locKey":"LOC_ROUTE_WATOPIA_TRIPLE_FLAT_LOOPS","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Triple_Flat_Loops.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":6.2,"leadinDistanceInMeters":2382.6,"blockedForMeetups":1,"sports":["CYCLING"]},{"name":"Mayan Bridge Loop","id":1082034232,"distanceInMeters":5273.5,"distanceInMetersFromEventStart":0.0,"ascentInMeters":48.3,"locKey":"LOC_ROUTE_WATOPIA_MAYAN_BRIDGE_LOOP","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Mayan_Bridge_Loop.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":9.4,"leadinDistanceInMeters":5658.0,"blockedForMeetups":1,"sports":["RUNNING"]},{"name":"Volcano Flat Reverse","id":1397026382,"distanceInMeters":12349.2,"distanceInMetersFromEventStart":0.0,"ascentInMeters":50.0,"locKey":"LOC_ROUTE_WATOPIA_VOLCANO_FLAT","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Volcano_Flat_Reverse.png","levelLocked":5,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":0.1,"leadinDistanceInMeters":237.0,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Climber's Gambit","id":3023359358,"distanceInMeters":27823.0,"distanceInMetersFromEventStart":0.0,"ascentInMeters":670.5,"locKey":"LOC_ROUTE_WATOPIA_CLIMBERS_GAMBIT","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Climbers_Gambit.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":false,"leadinAscentInMeters":0.1,"leadinDistanceInMeters":204.1,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Road to Sky","id":2663908549,"distanceInMeters":17495.6,"distanceInMetersFromEventStart":0.0,"ascentInMeters":1045.8,"locKey":"LOC_ROUTE_WATOPIA_ROAD_TO_SKY","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Road_to_Sky.png","levelLocked":6,"publicEventsOnly":false,"supportedLaps":false,"leadinAscentInMeters":0.0,"leadinDistanceInMeters":103.4,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Volcano Flat","id":3994934674,"distanceInMeters":12350.6,"distanceInMetersFromEventStart":0.0,"ascentInMeters":49.9,"locKey":"LOC_ROUTE_WATOPIA_VOLCANOFLAT","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Volcano_Flat.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.7,"leadinDistanceInMeters":492.4,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Tempus Fugit","id":2128890027,"distanceInMeters":17231.3,"distanceInMetersFromEventStart":0.0,"ascentInMeters":25.6,"locKey":"LOC_ROUTE_WATOPIA_TEMPUS_FUGIT","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Tempus_Fugit.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":6.2,"leadinDistanceInMeters":2355.5,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Three Sisters Reverse","id":4263172118,"distanceInMeters":45693.6,"distanceInMetersFromEventStart":0.0,"ascentInMeters":883.3,"locKey":"LOC_ROUTE_WATOPIA_THREE_SISTERS","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Three_Sisters_Reverse.png","levelLocked":5,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":0.1,"leadinDistanceInMeters":200.1,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"The Magnificent 8","id":2207442179,"distanceInMeters":28932.0,"distanceInMetersFromEventStart":0.0,"ascentInMeters":155.0,"locKey":"LOC_ROUTE_WATOPIA_THE_MAGNIFICENT_8","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_The_Magnificent_8.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":0.1,"leadinDistanceInMeters":211.0,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Handful Of Gravel","id":1993374659,"distanceInMeters":6130.4,"distanceInMetersFromEventStart":0.0,"ascentInMeters":74.9,"locKey":"LOC_ROUTE_WATOPIA_HANDFUL_OF_GRAVEL","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Handful_Of_Gravel.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":18.9,"leadinDistanceInMeters":4253.7,"blockedForMeetups":1,"sports":["CYCLING"]},{"name":"Big Foot Hills","id":3921412335,"distanceInMeters":67585.8,"distanceInMetersFromEventStart":0.0,"ascentInMeters":707.9,"locKey":"LOC_ROUTE_WATOPIA_BIG_FOOT_HILLS","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Big_Foot_Hills.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":6.2,"leadinDistanceInMeters":2374.8,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Volcano Circuit","id":686828068,"distanceInMeters":4103.5,"distanceInMetersFromEventStart":0.0,"ascentInMeters":20.4,"locKey":"LOC_ROUTE_WATOPIA_VOLCANOCIR","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Volcano_Circuit.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":19.6,"leadinDistanceInMeters":2813.5,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Whole Lotta Lava","id":982239385,"distanceInMeters":12293.9,"distanceInMetersFromEventStart":0.0,"ascentInMeters":160.4,"locKey":"LOC_ROUTE_WATOPIA_WHOLE_LOTTA_LAVA","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Whole_Lotta_Lava.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":29.7,"leadinDistanceInMeters":4910.6,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"2022 Bambino Fondo","id":3368626651,"distanceInMeters":52955.6,"distanceInMetersFromEventStart":0.0,"ascentInMeters":397.1,"locKey":"LOC_ROUTE_WATOPIA_BAMBINO22","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_2022_Bambino_Fondo.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":false,"leadinAscentInMeters":0.1,"leadinDistanceInMeters":294.6,"blockedForMeetups":1,"sports":["CYCLING","RUNNING"]},{"name":"Jon's Route","id":136957568,"distanceInMeters":12496.7,"distanceInMetersFromEventStart":0.0,"ascentInMeters":58.9,"locKey":"LOC_ROUTE_WATOPIA_JONS_ROUTE","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Jons_Route.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":false,"leadinAscentInMeters":0.0,"leadinDistanceInMeters":302.1,"blockedForMeetups":0,"sports":["RUNNING"]},{"name":"Dust In The Wind","id":2675063596,"distanceInMeters":52111.1,"distanceInMetersFromEventStart":0.0,"ascentInMeters":582.5,"locKey":"LOC_ROUTE_WATOPIA_DUST_IN_THE_WIND","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Dust_In_The_Wind.png","levelLocked":6,"publicEventsOnly":false,"supportedLaps":false,"leadinAscentInMeters":0.1,"leadinDistanceInMeters":340.7,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Handful of Gravel Run","id":2708527018,"distanceInMeters":6130.4,"distanceInMetersFromEventStart":0.0,"ascentInMeters":74.9,"locKey":"LOC_ROUTE_WATOPIA_HANDFUL_OF_GRAVEL_RUN","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Handful_of_Gravel_Run.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":18.9,"leadinDistanceInMeters":4253.7,"blockedForMeetups":1,"sports":["RUNNING"]},{"name":"Jungle Circuit Reverse","id":2839057126,"distanceInMeters":7861.8,"distanceInMetersFromEventStart":0.0,"ascentInMeters":82.7,"locKey":"LOC_ROUTE_WATOPIA_JUNGLE_CIRCUIT","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Jungle_Circuit_Reverse.png","levelLocked":5,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":23.2,"leadinDistanceInMeters":6319.1,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Watopia Hilly Route","id":2737483381,"distanceInMeters":9193.1,"distanceInMetersFromEventStart":0.0,"ascentInMeters":108.8,"locKey":"LOC_ROUTE_WATOPIA_HILLY","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Watopia_Hilly_Route.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.5,"leadinDistanceInMeters":465.9,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Flat Route Reverse","id":3811569265,"distanceInMeters":10269.1,"distanceInMetersFromEventStart":0.0,"ascentInMeters":61.2,"locKey":"LOC_ROUTE_WATOPIA_FLAT_ROUTE","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Flat_Route_Reverse.png","levelLocked":5,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":0.1,"leadinDistanceInMeters":200.5,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Zwift Gran Fondo","id":242381847,"distanceInMeters":97506.4,"distanceInMetersFromEventStart":0.0,"ascentInMeters":1196.1,"locKey":"LOC_ROUTE_WATOPIA_ZWIFT_GRAN_FONDO","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Zwift_Gran_Fondo.png","levelLocked":5,"publicEventsOnly":true,"supportedLaps":false,"leadinAscentInMeters":0.4,"leadinDistanceInMeters":456.5,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"5K Loop","id":3819095753,"distanceInMeters":4999.0,"distanceInMetersFromEventStart":0.0,"ascentInMeters":30.5,"locKey":"LOC_ROUTE_WATOPIA_5K_LOOP","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_5K_Loop.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.0,"leadinDistanceInMeters":306.7,"blockedForMeetups":0,"sports":["RUNNING"]},{"name":"Eastern Eight","id":2746475460,"distanceInMeters":51679.8,"distanceInMetersFromEventStart":0.0,"ascentInMeters":406.9,"locKey":"LOC_ROUTE_WATOPIA_EASTERN_EIGHT","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Eastern_Eight.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":6.2,"leadinDistanceInMeters":2382.6,"blockedForMeetups":1,"sports":["CYCLING"]},{"name":"Zwift Medio Fondo","id":3748780161,"distanceInMeters":72506.4,"distanceInMetersFromEventStart":0.0,"ascentInMeters":1009.5,"locKey":"LOC_ROUTE_WATOPIA_MEDIO_FONDO","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Zwift_Medio_Fondo.png","levelLocked":5,"publicEventsOnly":true,"supportedLaps":false,"leadinAscentInMeters":0.7,"leadinDistanceInMeters":492.4,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Zwift Bambino Fondo","id":3621162212,"distanceInMeters":51885.0,"distanceInMetersFromEventStart":0.0,"ascentInMeters":579.7,"locKey":"LOC_ROUTE_WATOPIA_BAMBINO_FONDO","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Zwift_Bambino_Fondo.png","levelLocked":5,"publicEventsOnly":true,"supportedLaps":false,"leadinAscentInMeters":0.7,"leadinDistanceInMeters":492.4,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"2022 Medio Fondo","id":2900074211,"distanceInMeters":79023.4,"distanceInMetersFromEventStart":0.0,"ascentInMeters":921.9,"locKey":"LOC_ROUTE_WATOPIA_MEDIO22","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_2022_Medio_Fondo.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":false,"leadinAscentInMeters":0.1,"leadinDistanceInMeters":295.0,"blockedForMeetups":1,"sports":["CYCLING","RUNNING"]},{"name":"WBR Climbing Series","id":2218409282,"distanceInMeters":43208.5,"distanceInMetersFromEventStart":0.0,"ascentInMeters":1115.9,"locKey":"LOC_ROUTE_WATOPIA_WBR_CLIMBING_SERIES","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_WBR_Climbing_Series.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":false,"leadinAscentInMeters":0.5,"leadinDistanceInMeters":465.9,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Beach Island Loop","id":2474227587,"distanceInMeters":12780.6,"distanceInMetersFromEventStart":0.0,"ascentInMeters":48.7,"locKey":"LOC_ROUTE_WATOPIA_BEACH_ISLAND_LOOP","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Beach_Island_Loop.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.1,"leadinDistanceInMeters":327.9,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Hilly Route Reverse","id":107363867,"distanceInMeters":9192.8,"distanceInMetersFromEventStart":0.0,"ascentInMeters":108.9,"locKey":"LOC_ROUTE_WATOPIA_HILLY_ROUTE","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Hilly_Route_Reverse.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":0.1,"leadinDistanceInMeters":230.2,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Serpentine 8","id":2829629527,"distanceInMeters":19264.7,"distanceInMetersFromEventStart":0.0,"ascentInMeters":206.0,"locKey":"LOC_ROUTE_WATOPIA_SERPENTINE_8","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Serpentine_8.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":33.0,"leadinDistanceInMeters":7493.3,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"2022 Gran Fondo","id":1327147942,"distanceInMeters":92467.2,"distanceInMetersFromEventStart":0.0,"ascentInMeters":1116.0,"locKey":"LOC_ROUTE_WATOPIA_GRAND22","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_2022_Gran_Fondo.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":false,"leadinAscentInMeters":0.1,"leadinDistanceInMeters":294.6,"blockedForMeetups":1,"sports":["CYCLING","RUNNING"]},{"name":"Chili Pepper","id":1373909093,"distanceInMeters":7956.9,"distanceInMetersFromEventStart":0.0,"ascentInMeters":47.8,"locKey":"LOC_ROUTE_WATOPIA_CHILI_PEPPER","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Chili_Pepper.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.0,"leadinDistanceInMeters":301.7,"blockedForMeetups":0,"sports":["RUNNING"]},{"name":"Watopia Figure 8 Reverse","id":553661379,"distanceInMeters":29697.9,"distanceInMetersFromEventStart":0.0,"ascentInMeters":254.0,"locKey":"LOC_ROUTE_WATOPIA_FIGURE8","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Watopia_Figure_8_Reverse.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.1,"leadinDistanceInMeters":325.7,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Tour of Fire and Ice","id":1766405776,"distanceInMeters":25361.0,"distanceInMetersFromEventStart":0.0,"ascentInMeters":1164.4,"locKey":"LOC_ROUTE_WATOPIA_TOUR_OF_FIRE_AND_ICE","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Tour_of_Fire_and_Ice.png","levelLocked":6,"publicEventsOnly":false,"supportedLaps":false,"leadinAscentInMeters":19.6,"leadinDistanceInMeters":2764.6,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Two Bridges Loop","id":3312037616,"distanceInMeters":7110.2,"distanceInMetersFromEventStart":0.0,"ascentInMeters":80.7,"locKey":"LOC_ROUTE_WATOPIA_TWO_BRIDGES_LOOP","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Two_Bridges_Loop.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.1,"leadinDistanceInMeters":477.2,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Quatch Quest","id":2969952077,"distanceInMeters":46469.9,"distanceInMetersFromEventStart":0.0,"ascentInMeters":1706.6,"locKey":"LOC_ROUTE_WATOPIA_QUATCH_QUEST","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Quatch_Quest.png","levelLocked":6,"publicEventsOnly":false,"supportedLaps":false,"leadinAscentInMeters":0.1,"leadinDistanceInMeters":331.3,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"That's Amore Reverse","id":1586193601,"distanceInMeters":5829.5,"distanceInMetersFromEventStart":0.0,"ascentInMeters":53.5,"locKey":"LOC_ROUTE_WATOPIA_THATS_AMORE","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Thats_Amore_Reverse.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":0.8,"leadinDistanceInMeters":124.6,"blockedForMeetups":0,"sports":["RUNNING"]},{"name":"The Uber Pretzel","id":1475638265,"distanceInMeters":128299.2,"distanceInMetersFromEventStart":0.0,"ascentInMeters":2379.9,"locKey":"LOC_ROUTE_WATOPIA_THE_UBER_PRETZEL","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_The_Uber_Pretzel.png","levelLocked":6,"publicEventsOnly":false,"supportedLaps":false,"leadinAscentInMeters":1.1,"leadinDistanceInMeters":523.1,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Downtown Titans","id":5745690,"distanceInMeters":24645.0,"distanceInMetersFromEventStart":0.0,"ascentInMeters":292.1,"locKey":"LOC_ROUTE_WATOPIA_DOWNTOWN_TITANS","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Downtown_Titans.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":3.0,"leadinDistanceInMeters":788.9,"blockedForMeetups":1,"sports":["CYCLING"]},{"name":"Legends and Lava","id":3864857876,"distanceInMeters":24567.2,"distanceInMetersFromEventStart":0.0,"ascentInMeters":352.3,"locKey":"LOC_ROUTE_WATOPIA_LEGENDS_AND_LAVA","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Legends_and_Lava.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":false,"leadinAscentInMeters":0.0,"leadinDistanceInMeters":97.4,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Road to Ruins","id":3701568815,"distanceInMeters":29647.2,"distanceInMetersFromEventStart":0.0,"ascentInMeters":274.8,"locKey":"LOC_ROUTE_WATOPIA_ASPHALT_JUNGLE","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Road_to_Ruins.png","levelLocked":5,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":1.0,"leadinDistanceInMeters":516.9,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Bigger Loop","id":2139708890,"distanceInMeters":53197.9,"distanceInMetersFromEventStart":0.0,"ascentInMeters":691.2,"locKey":"LOC_ROUTE_WATOPIA_BIGGER_LOOP","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Bigger_Loop.png","levelLocked":5,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.9,"leadinDistanceInMeters":506.0,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Big Loop Reverse","id":4107844490,"distanceInMeters":42601.6,"distanceInMetersFromEventStart":0.0,"ascentInMeters":661.7,"locKey":"LOC_ROUTE_WATOPIA_BIG_LOOP","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Big_Loop_Reverse.png","levelLocked":5,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":0.1,"leadinDistanceInMeters":217.6,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Tick Tock","id":3366225080,"distanceInMeters":16861.1,"distanceInMetersFromEventStart":0.0,"ascentInMeters":53.1,"locKey":"LOC_ROUTE_WATOPIA_TICK_TOCK","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Tick_Tock.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":6.2,"leadinDistanceInMeters":2355.5,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"The Mega Pretzel","id":3848492017,"distanceInMeters":107274.9,"distanceInMetersFromEventStart":0.0,"ascentInMeters":1638.4,"locKey":"LOC_ROUTE_WATOPIA_MEGA_PRETZEL","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_The_Mega_Pretzel.png","levelLocked":5,"publicEventsOnly":false,"supportedLaps":false,"leadinAscentInMeters":20.6,"leadinDistanceInMeters":3709.4,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Volcano Climb After Party","id":387309391,"distanceInMeters":39853.9,"distanceInMetersFromEventStart":0.0,"ascentInMeters":284.9,"locKey":"LOC_ROUTE_WATOPIA_PACK_VOLCANO_CLIMB_PARTY","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Volcano_Climb_After_Party.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":false,"leadinAscentInMeters":0.5,"leadinDistanceInMeters":465.9,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Chili Pepper Reverse","id":3694952104,"distanceInMeters":7346.9,"distanceInMetersFromEventStart":0.0,"ascentInMeters":46.5,"locKey":"LOC_ROUTE_WATOPIA_CHILI_PEPPER","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Chili_Pepper_Reverse.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":1.5,"leadinDistanceInMeters":154.9,"blockedForMeetups":0,"sports":["RUNNING"]},{"name":"That's Amore","id":263936293,"distanceInMeters":6439.4,"distanceInMetersFromEventStart":0.0,"ascentInMeters":54.7,"locKey":"LOC_ROUTE_WATOPIA_THATS_AMORE","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Thats_Amore.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.0,"leadinDistanceInMeters":288.4,"blockedForMeetups":0,"sports":["RUNNING"]},{"name":"Watopia's Waistband","id":1064303857,"distanceInMeters":25456.2,"distanceInMetersFromEventStart":0.0,"ascentInMeters":95.1,"locKey":"LOC_ROUTE_WATOPIA_WATOPIAS_WAISTBAND","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Watopias_Waistband.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":6.2,"leadinDistanceInMeters":2354.2,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]}]},{"name":"RICHMOND","routes":[{"name":"Libby Hill After Party","id":54700404,"distanceInMeters":32856.4,"distanceInMetersFromEventStart":0.0,"ascentInMeters":105.0,"locKey":"LOC_ROUTE_RICHMOND_LIBBY_HILL_AFTER_PARTY","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/RICHMOND_Libby_Hill_After_Party.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":false,"leadinAscentInMeters":2.8,"leadinDistanceInMeters":315.2,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Cobbled Climbs","id":1545087483,"distanceInMeters":9178.4,"distanceInMetersFromEventStart":0.0,"ascentInMeters":133.5,"locKey":"LOC_ROUTE_RICHMOND_PREFERHILLY","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/RICHMOND_Cobbled_Climbs.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":2.8,"leadinDistanceInMeters":319.3,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Richmond UCI Worlds","id":2196019512,"distanceInMeters":16248.6,"distanceInMetersFromEventStart":0.0,"ascentInMeters":157.6,"locKey":"LOC_ROUTE_RICHMOND_UCI","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/RICHMOND_Richmond_UCI_Worlds.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":3.3,"leadinDistanceInMeters":503.8,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Richmond Rollercoaster","id":948831673,"distanceInMeters":5057.2,"distanceInMetersFromEventStart":0.0,"ascentInMeters":19.9,"locKey":"LOC_ROUTE_RICHMOND_FAN_FLATS_REVERSE","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/RICHMOND_Richmond_Rollercoaster.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":150.0,"leadinDistanceInMeters":12146.6,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"The Fan Flats","id":1638640398,"distanceInMeters":5056.3,"distanceInMetersFromEventStart":0.0,"ascentInMeters":19.8,"locKey":"LOC_ROUTE_RICHMOND_PREFERFLAT","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/RICHMOND_The_Fan_Flats.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":20.1,"leadinDistanceInMeters":4263.5,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Cobbled Climbs Reverse","id":4194352271,"distanceInMeters":9177.4,"distanceInMetersFromEventStart":0.0,"ascentInMeters":133.6,"locKey":"LOC_ROUTE_RICHMOND_COBBLED_CLIMBS_REVERSE","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/RICHMOND_Cobbled_Climbs_Reverse.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":0.0,"leadinDistanceInMeters":99.7,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Richmond UCI Reverse","id":1039983620,"distanceInMeters":16249.1,"distanceInMetersFromEventStart":0.0,"ascentInMeters":157.6,"locKey":"LOC_ROUTE_RICHMOND_RICHMOND_UCI_REVERSE","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/RICHMOND_Richmond_UCI_Reverse.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":0.0,"leadinDistanceInMeters":99.7,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]}]},{"name":"LONDON","routes":[{"name":"London Classique","id":2694166390,"distanceInMeters":5458.5,"distanceInMetersFromEventStart":0.0,"ascentInMeters":25.2,"locKey":"LOC_ROUTE_LONDON_CLASSIQUE","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/LONDON_London_Classique.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":44.8,"leadinDistanceInMeters":5691.3,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"London PRL Half","id":764532081,"distanceInMeters":69158.6,"distanceInMetersFromEventStart":0.0,"ascentInMeters":1009.3,"locKey":"LOC_ROUTE_LONDON_PRLHALF","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/LONDON_London_PRL_Half.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":false,"leadinAscentInMeters":4.2,"leadinDistanceInMeters":456.9,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Greater London Flat","id":928793662,"distanceInMeters":11677.6,"distanceInMetersFromEventStart":0.0,"ascentInMeters":53.2,"locKey":"LOC_ROUTE_LONDON_GREATERLONDONFLAT","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/LONDON_Greater_London_Flat.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":44.8,"leadinDistanceInMeters":5688.2,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"London 8","id":4012646479,"distanceInMeters":20341.1,"distanceInMetersFromEventStart":0.0,"ascentInMeters":255.8,"locKey":"LOC_ROUTE_LONDON_LONDON8","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/LONDON_London_8.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":4.7,"leadinDistanceInMeters":479.1,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Greater London Loop Reverse","id":474781994,"distanceInMeters":20977.7,"distanceInMetersFromEventStart":0.0,"ascentInMeters":255.0,"locKey":"LOC_ROUTE_LONDON_GREATER_LONDON_LOOP","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/LONDON_Greater_London_Loop_Reverse.png","levelLocked":1,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":0.7,"leadinDistanceInMeters":176.2,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Greater London 8","id":87055383,"distanceInMeters":23807.7,"distanceInMetersFromEventStart":0.0,"ascentInMeters":275.7,"locKey":"LOC_ROUTE_LONDON_GREATERLONDON8","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/LONDON_Greater_London_8.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":4.7,"leadinDistanceInMeters":479.1,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"London Classique Reverse","id":3599973269,"distanceInMeters":5457.0,"distanceInMetersFromEventStart":0.0,"ascentInMeters":25.2,"locKey":"LOC_ROUTE_LONDON_CLASSIQUE","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/LONDON_London_Classique_Reverse.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":59.5,"leadinDistanceInMeters":7462.8,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"The London Pretzel","id":163688809,"distanceInMeters":55735.7,"distanceInMetersFromEventStart":0.0,"ascentInMeters":572.0,"locKey":"LOC_ROUTE_LONDON_PRETZEL","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/LONDON_The_London_Pretzel.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":4.7,"leadinDistanceInMeters":479.1,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Greatest London Loop Reverse","id":3976402826,"distanceInMeters":25671.2,"distanceInMetersFromEventStart":0.0,"ascentInMeters":355.1,"locKey":"LOC_ROUTE_LONDON_GREATEST_LONDON_LOOP","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/LONDON_Greatest_London_Loop_Reverse.png","levelLocked":1,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":0.7,"leadinDistanceInMeters":174.5,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"London 8 Reverse","id":2165880404,"distanceInMeters":20291.4,"distanceInMetersFromEventStart":0.0,"ascentInMeters":255.7,"locKey":"LOC_ROUTE_LONDON_LONDON_8","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/LONDON_London_8_Reverse.png","levelLocked":10,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":0.7,"leadinDistanceInMeters":174.5,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"London Loop","id":913172163,"distanceInMeters":14883.7,"distanceInMetersFromEventStart":0.0,"ascentInMeters":230.7,"locKey":"LOC_ROUTE_LONDON_LOOP","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/LONDON_London_Loop.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":4.7,"leadinDistanceInMeters":479.1,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"London Loop Reverse","id":1788889233,"distanceInMeters":14834.2,"distanceInMetersFromEventStart":0.0,"ascentInMeters":230.5,"locKey":"LOC_ROUTE_LONDON_LONDON_LOOP","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/LONDON_London_Loop_Reverse.png","levelLocked":10,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":0.7,"leadinDistanceInMeters":174.5,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Greatest London Flat","id":1880443431,"distanceInMeters":23608.3,"distanceInMetersFromEventStart":0.0,"ascentInMeters":163.4,"locKey":"LOC_ROUTE_LONDON_GREATEST_LONDON_FLAT","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/LONDON_Greatest_London_Flat.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":59.5,"leadinDistanceInMeters":7463.9,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Leith Hill After Party","id":1230300449,"distanceInMeters":41542.7,"distanceInMetersFromEventStart":0.0,"ascentInMeters":434.3,"locKey":"LOC_ROUTE_LONDON_LEITH_HILL_AFTER_PARTY","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/LONDON_Leith_Hill_After_Party.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":false,"leadinAscentInMeters":4.4,"leadinDistanceInMeters":464.8,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Surrey Hills","id":3707791029,"distanceInMeters":39125.1,"distanceInMetersFromEventStart":0.0,"ascentInMeters":877.3,"locKey":"LOC_ROUTE_LONDON_SURRYHILLS","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/LONDON_Surrey_Hills.png","levelLocked":1,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":156.8,"leadinDistanceInMeters":5043.5,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Greatest London Loop","id":3853654821,"distanceInMeters":25666.4,"distanceInMetersFromEventStart":0.0,"ascentInMeters":355.2,"locKey":"LOC_ROUTE_LONDON_GREATESTLONDONLOOP","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/LONDON_Greatest_London_Loop.png","levelLocked":1,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":4.7,"leadinDistanceInMeters":479.1,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"London PRL FULL","id":2204461619,"distanceInMeters":173333.4,"distanceInMetersFromEventStart":0.0,"ascentInMeters":2623.3,"locKey":"LOC_ROUTE_LONDON_PRLFULL","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/LONDON_London_PRL_FULL.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":false,"leadinAscentInMeters":4.2,"leadinDistanceInMeters":456.9,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Keith Hill After Party","id":3569674525,"distanceInMeters":36188.4,"distanceInMetersFromEventStart":0.0,"ascentInMeters":431.0,"locKey":"LOC_ROUTE_LONDON_KEITH_HILL_AFTER_PARTY","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/LONDON_Keith_Hill_After_Party.png","levelLocked":1,"publicEventsOnly":true,"supportedLaps":false,"leadinAscentInMeters":4.4,"leadinDistanceInMeters":464.2,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Triple Loops","id":4210048937,"distanceInMeters":40945.1,"distanceInMetersFromEventStart":0.0,"ascentInMeters":564.4,"locKey":"LOC_ROUTE_LONDON_TRIPLELOOPS","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/LONDON_Triple_Loops.png","levelLocked":1,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":4.7,"leadinDistanceInMeters":479.1,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Greater London Loop","id":3276403604,"distanceInMeters":21028.7,"distanceInMetersFromEventStart":0.0,"ascentInMeters":255.0,"locKey":"LOC_ROUTE_LONDON_GREATERLONDONLOOP","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/LONDON_Greater_London_Loop.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":4.7,"leadinDistanceInMeters":479.1,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]}]},{"name":"INNSBRUCK","routes":[{"name":"Lutscher","id":156457316,"distanceInMeters":13704.5,"distanceInMetersFromEventStart":0.0,"ascentInMeters":401.7,"locKey":"LOC_ROUTE_INNSBRUCK_LUTSCHER","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/INNSBRUCK_Lutscher.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":426.9,"leadinDistanceInMeters":10888.3,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Innsbruckring","id":2592027600,"distanceInMeters":8799.1,"distanceInMetersFromEventStart":0.0,"ascentInMeters":77.0,"locKey":"LOC_ROUTE_INNSBRUCK_INNSBRUCKRING","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/INNSBRUCK_Innsbruckring.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.8,"leadinDistanceInMeters":233.3,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Lutscher CCW","id":3801241714,"distanceInMeters":13750.4,"distanceInMetersFromEventStart":0.0,"ascentInMeters":401.7,"locKey":"LOC_ROUTE_INNSBRUCK_LUTSCHERCCW","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/INNSBRUCK_Lutscher_CCW.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":425.4,"leadinDistanceInMeters":8944.4,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Innsbruck KOM After Party","id":3649347250,"distanceInMeters":36972.6,"distanceInMetersFromEventStart":0.0,"ascentInMeters":657.0,"locKey":"LOC_ROUTE_INNSBRUCK_INNSBRUCK_KOM_AFTER_PARTY_3X","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/INNSBRUCK_Innsbruck_KOM_After_Party.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":false,"leadinAscentInMeters":0.8,"leadinDistanceInMeters":224.3,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Achterbahn","id":4009235104,"distanceInMeters":47426.4,"distanceInMetersFromEventStart":0.0,"ascentInMeters":989.0,"locKey":"LOC_ROUTE_INNSBRUCK_ACHTERBAHN","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/INNSBRUCK_Achterbahn.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.8,"leadinDistanceInMeters":233.3,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"2018 UCI Worlds Short Lap","id":3114603308,"distanceInMeters":23682.4,"distanceInMetersFromEventStart":0.0,"ascentInMeters":494.8,"locKey":"LOC_ROUTE_INNSBRUCK_UCISHORT","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/INNSBRUCK_2018_UCI_Worlds_Short_Lap.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.8,"leadinDistanceInMeters":233.3,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]}]},{"name":"MAKURIISLANDS","routes":[{"name":"Electric Loop","id":910684583,"distanceInMeters":8941.4,"distanceInMetersFromEventStart":0.0,"ascentInMeters":41.7,"locKey":"LOC_ROUTE_MAKURIISLANDS_ELECTRIC_LOOP","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/MAKURIISLANDS_Electric_Loop.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.8,"leadinDistanceInMeters":40.8,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Valley to Mountaintop","id":1941800093,"distanceInMeters":5007.0,"distanceInMetersFromEventStart":0.0,"ascentInMeters":130.5,"locKey":"LOC_ROUTE_JAPAN_VALLEY_TO_MOUNTAINTOP","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/MAKURIISLANDS_Valley_to_Mountaintop.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":false,"leadinAscentInMeters":0.2,"leadinDistanceInMeters":122.3,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Sprinter's Playground","id":3356878261,"distanceInMeters":12342.6,"distanceInMetersFromEventStart":0.0,"ascentInMeters":67.4,"locKey":"LOC_ROUTE_MAKURIISLANDS_SPRINTERS_PLAYGROUND","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/MAKURIISLANDS_Sprinters_Playground.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.1,"leadinDistanceInMeters":256.8,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Two Village Loop","id":2653858696,"distanceInMeters":12804.3,"distanceInMetersFromEventStart":0.0,"ascentInMeters":88.2,"locKey":"LOC_ROUTE_JAPAN_TWO_VILLAGE_LOOP","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/MAKURIISLANDS_Two_Village_Loop.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.2,"leadinDistanceInMeters":273.2,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Chasing the Sun","id":2866480562,"distanceInMeters":35050.1,"distanceInMetersFromEventStart":0.0,"ascentInMeters":314.8,"locKey":"LOC_ROUTE_MAKURIISLANDS_CHASING_THE_SUN","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/MAKURIISLANDS_Chasing_the_Sun.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.7,"leadinDistanceInMeters":37.6,"blockedForMeetups":0,"sports":["CYCLING"]},{"name":"Sea to Tree","id":3603635554,"distanceInMeters":3272.8,"distanceInMetersFromEventStart":0.0,"ascentInMeters":107.5,"locKey":"LOC_ROUTE_JAPAN_SEA_TO_TREE","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/MAKURIISLANDS_Sea_to_Tree.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":false,"leadinAscentInMeters":13.8,"leadinDistanceInMeters":572.1,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Neokyo All-Nighter","id":1453570384,"distanceInMeters":24331.9,"distanceInMetersFromEventStart":0.0,"ascentInMeters":167.6,"locKey":"LOC_ROUTE_MAKURIISLANDS_NEOKYO_ALL-NIGHTER","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/MAKURIISLANDS_Neokyo_All_Nighter.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.2,"leadinDistanceInMeters":277.7,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Kappa Quest Reverse","id":1454553567,"distanceInMeters":9086.6,"distanceInMetersFromEventStart":0.0,"ascentInMeters":139.9,"locKey":"LOC_ROUTE_JAPAN_KAPPA_QUEST_REVERSE","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/MAKURIISLANDS_Kappa_Quest_Reverse.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":130.3,"leadinDistanceInMeters":5156.4,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Chain Chomper","id":3691918883,"distanceInMeters":13615.8,"distanceInMetersFromEventStart":0.0,"ascentInMeters":183.4,"locKey":"LOC_ROUTE_JAPAN_CHAIN_CHOMPER","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/MAKURIISLANDS_Chain_Chomper.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":5.3,"leadinDistanceInMeters":2418.7,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Railways and Rooftops","id":246712730,"distanceInMeters":6195.8,"distanceInMetersFromEventStart":0.0,"ascentInMeters":70.4,"locKey":"LOC_ROUTE_MAKURIISLANDS_RAILWAYS_AND_ROOFTOPS","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/MAKURIISLANDS_Railways_and_Rooftops.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":21.1,"leadinDistanceInMeters":2174.6,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Rooftop Rendezvous","id":3565430790,"distanceInMeters":3744.6,"distanceInMetersFromEventStart":0.0,"ascentInMeters":56.3,"locKey":"LOC_ROUTE_MAKURIISLANDS_ROOFTOP_RENDEZVOUS","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/MAKURIISLANDS_Rooftop_Rendezvous.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":25.6,"leadinDistanceInMeters":2938.5,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Farmland Loop","id":2980619755,"distanceInMeters":7758.1,"distanceInMetersFromEventStart":0.0,"ascentInMeters":57.2,"locKey":"LOC_ROUTE_JAPAN_FARMLAND_LOOP","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/MAKURIISLANDS_Farmland_Loop.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.2,"leadinDistanceInMeters":229.4,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Wandering Flats","id":3914529041,"distanceInMeters":25103.1,"distanceInMetersFromEventStart":0.0,"ascentInMeters":145.6,"locKey":"LOC_ROUTE_MAKURIISLANDS_WANDERING_FLATS","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/MAKURIISLANDS_Wandering_Flats.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.2,"leadinDistanceInMeters":110.9,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Flatland Loop","id":3282611437,"distanceInMeters":12855.4,"distanceInMetersFromEventStart":0.0,"ascentInMeters":99.3,"locKey":"LOC_ROUTE_JAPAN_FLATLAND_LOOP","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/MAKURIISLANDS_Flatland_Loop.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.2,"leadinDistanceInMeters":253.6,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Neokyo Crit Course","id":1127056801,"distanceInMeters":3882.0,"distanceInMetersFromEventStart":0.0,"ascentInMeters":19.7,"locKey":"LOC_ROUTE_MAKURIISLANDS_NEOKYO_CRIT_COURSE","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/MAKURIISLANDS_Neokyo_Crit_Course.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":4.8,"leadinDistanceInMeters":778.0,"blockedForMeetups":0,"sports":["CYCLING"]},{"name":"Castle to Castle","id":3742187716,"distanceInMeters":22379.8,"distanceInMetersFromEventStart":0.0,"ascentInMeters":139.7,"locKey":"LOC_ROUTE_MAKURIISLANDS_CASTLE_TO_CASTLE","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/MAKURIISLANDS_Castle_to_Castle.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":1.0,"leadinDistanceInMeters":817.8,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Spirit Forest","id":3523806426,"distanceInMeters":8447.6,"distanceInMetersFromEventStart":0.0,"ascentInMeters":134.8,"locKey":"LOC_ROUTE_JAPAN_SPIRIT_FOREST","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/MAKURIISLANDS_Spirit_Forest.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":122.0,"leadinDistanceInMeters":4650.6,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Sleepless City","id":3302953739,"distanceInMeters":9559.2,"distanceInMetersFromEventStart":0.0,"ascentInMeters":42.5,"locKey":"LOC_ROUTE_MAKURIISLANDS_SLEEPLESS_CITY","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/MAKURIISLANDS_Sleepless_City.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.0,"leadinDistanceInMeters":51.0,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Twilight Harbor","id":1457923570,"distanceInMeters":6854.4,"distanceInMetersFromEventStart":0.0,"ascentInMeters":33.1,"locKey":"LOC_ROUTE_MAKURIISLANDS_TWILIGHT_HARBOR","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/MAKURIISLANDS_Twilight_Harbor.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":3.9,"leadinDistanceInMeters":248.1,"blockedForMeetups":0,"sports":["CYCLING"]},{"name":"Neon Flats","id":3407362320,"distanceInMeters":14739.8,"distanceInMetersFromEventStart":0.0,"ascentInMeters":72.0,"locKey":"LOC_ROUTE_MAKURIISLANDS_NEON_FLATS","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/MAKURIISLANDS_Neon_Flats.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.1,"leadinDistanceInMeters":260.1,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Kappa Quest","id":1562187590,"distanceInMeters":9085.1,"distanceInMetersFromEventStart":0.0,"ascentInMeters":139.9,"locKey":"LOC_ROUTE_JAPAN_KAPPA_QUEST","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/MAKURIISLANDS_Kappa_Quest.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":121.6,"leadinDistanceInMeters":3896.1,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Three Village Loop","id":3379779247,"distanceInMeters":10553.8,"distanceInMetersFromEventStart":0.0,"ascentInMeters":92.6,"locKey":"LOC_ROUTE_JAPAN_THREE_VILLAGE_LOOP","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/MAKURIISLANDS_Three_Village_Loop.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":5.3,"leadinDistanceInMeters":2419.3,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Countryside Tour","id":525689100,"distanceInMeters":15849.5,"distanceInMetersFromEventStart":0.0,"ascentInMeters":185.2,"locKey":"LOC_ROUTE_JAPAN_COUNTRYSIDE_TOUR","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/MAKURIISLANDS_Countryside_Tour.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.2,"leadinDistanceInMeters":230.2,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Temples and Towers","id":2896159042,"distanceInMeters":32594.2,"distanceInMetersFromEventStart":0.0,"ascentInMeters":318.3,"locKey":"LOC_ROUTE_MAKURIISLANDS_TEMPLES_AND_TOWERS","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/MAKURIISLANDS_Temples_and_Towers.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":1.1,"leadinDistanceInMeters":778.5,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Suki's Playground","id":3367186349,"distanceInMeters":18311.0,"distanceInMetersFromEventStart":0.0,"ascentInMeters":149.8,"locKey":"LOC_ROUTE_JAPAN_SUKIS_PLAYGROUND","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/MAKURIISLANDS_Sukis_Playground.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.2,"leadinDistanceInMeters":216.7,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]}]},{"name":"YORKSHIRE","routes":[{"name":"Harrogate Circuit Reverse","id":620436060,"distanceInMeters":13832.9,"distanceInMetersFromEventStart":0.0,"ascentInMeters":245.2,"locKey":"LOC_ROUTE_YORKSHIRE_HARROGATE_CIRCUIT","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/YORKSHIRE_Harrogate_Circuit_Reverse.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":0.0,"leadinDistanceInMeters":22.3,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"2019 UCI Worlds Harrogate Circuit","id":2007026433,"distanceInMeters":13834.4,"distanceInMetersFromEventStart":0.0,"ascentInMeters":245.2,"locKey":"LOC_ROUTE_YORKSHIRE_HARROGATE_CIRCUIT","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/YORKSHIRE_2019_UCI_Worlds_Harrogate_Circuit.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":1.3,"leadinDistanceInMeters":118.0,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Queen's Highway","id":3007266671,"distanceInMeters":2999.4,"distanceInMetersFromEventStart":0.0,"ascentInMeters":40.6,"locKey":"LOC_ROUTE_YORKSHIRE_QUEENS","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/YORKSHIRE_Queens_Highway.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":41.9,"leadinDistanceInMeters":2764.1,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Duchy Estate","id":1233527301,"distanceInMeters":2999.5,"distanceInMetersFromEventStart":0.0,"ascentInMeters":40.6,"locKey":"LOC_ROUTE_YORKSHIRE_DUCHY","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/YORKSHIRE_Duchy_Estate.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":29.2,"leadinDistanceInMeters":1718.6,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Tour Of Tewit Well","id":1086718516,"distanceInMeters":10876.6,"distanceInMetersFromEventStart":0.0,"ascentInMeters":204.5,"locKey":"LOC_ROUTE_YORKSHIRE_TEWIT","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/YORKSHIRE_Tour_Of_Tewit_Well.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.0,"leadinDistanceInMeters":27.3,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Royal Pump Room 8","id":2905381067,"distanceInMeters":27712.1,"distanceInMetersFromEventStart":0.0,"ascentInMeters":490.5,"locKey":"LOC_ROUTE_YORKSHIRE_8","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/YORKSHIRE_Royal_Pump_Room_8.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.0,"leadinDistanceInMeters":21.8,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]}]},{"name":"FRANCE","routes":[{"name":"Tire-Bouchon","id":2413440572,"distanceInMeters":60770.9,"distanceInMetersFromEventStart":0.0,"ascentInMeters":482.8,"locKey":"LOC_ROUTE_FRANCE_TIRE_BOUCHON","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/FRANCE_Tire_Bouchon.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":107.9,"leadinDistanceInMeters":3113.6,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Roule Ma Poule","id":872351836,"distanceInMeters":22888.8,"distanceInMetersFromEventStart":0.0,"ascentInMeters":155.0,"locKey":"LOC_ROUTE_FRANCE_ROULE_MA_POULE","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/FRANCE_Roule_Ma_Poule.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":108.0,"leadinDistanceInMeters":4310.3,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Casse-Pattes","id":3919912289,"distanceInMeters":22888.0,"distanceInMetersFromEventStart":0.0,"ascentInMeters":155.1,"locKey":"LOC_ROUTE_FRANCE_CASSE_PATTES","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/FRANCE_Casse_Pattes.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.0,"leadinDistanceInMeters":876.5,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Ven-Top","id":2573468147,"distanceInMeters":20750.8,"distanceInMetersFromEventStart":0.0,"ascentInMeters":1539.2,"locKey":"LOC_ROUTE_FRANCE_VEN_TOP","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/FRANCE_Ven_Top.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":false,"leadinAscentInMeters":0.1,"leadinDistanceInMeters":153.1,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"R.G.V.","id":1776635757,"distanceInMeters":23998.9,"distanceInMetersFromEventStart":0.0,"ascentInMeters":133.3,"locKey":"LOC_ROUTE_FRANCE_RGV","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/FRANCE_R_G_V_.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.0,"leadinDistanceInMeters":955.1,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Douce France","id":986252325,"distanceInMeters":24007.5,"distanceInMetersFromEventStart":0.0,"ascentInMeters":133.3,"locKey":"LOC_ROUTE_FRANCE_DOUCE_FRANCE","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/FRANCE_Douce_France.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":3.3,"leadinDistanceInMeters":779.7,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"La Reine","id":1433431343,"distanceInMeters":22458.7,"distanceInMetersFromEventStart":0.0,"ascentInMeters":1205.2,"locKey":"LOC_ROUTE_FRANCE_LA_REINE","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/FRANCE_La_Reine.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":false,"leadinAscentInMeters":0.0,"leadinDistanceInMeters":448.9,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Petit Boucle","id":2852153296,"distanceInMeters":60782.4,"distanceInMetersFromEventStart":0.0,"ascentInMeters":483.1,"locKey":"LOC_ROUTE_FRANCE_PETIT_BOUCLE","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/FRANCE_Petit_Boucle.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":0.0,"leadinDistanceInMeters":979.4,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]}]},{"name":"CRITCITY","routes":[{"name":"The Bell Lap","id":2875658892,"distanceInMeters":1961.7,"distanceInMetersFromEventStart":0.0,"ascentInMeters":17.0,"locKey":"LOC_ROUTE_ESPORTS1_THE_BELL_LAP","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/CRITCITY_The_Bell_Lap.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":0.0,"leadinDistanceInMeters":98.7,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Downtown Dolphin","id":947394567,"distanceInMeters":1961.8,"distanceInMetersFromEventStart":0.0,"ascentInMeters":17.0,"locKey":"LOC_ROUTE_ESPORTS1_DOWNTOWN_DOLPHIN","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/CRITCITY_Downtown_Dolphin.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":true,"leadinAscentInMeters":0.0,"leadinDistanceInMeters":70.8,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]}]},{"name":"PARIS","routes":[{"name":"Champs-Élysées","id":3364574135,"distanceInMeters":6616.1,"distanceInMetersFromEventStart":0.0,"ascentInMeters":39.4,"locKey":"LOC_ROUTE_PARIS_CHAMPS_ELYSEES","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/PARIS_Champs__lys_es.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":13.4,"leadinDistanceInMeters":3206.6,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Lutece Express","id":1236439870,"distanceInMeters":6616.1,"distanceInMetersFromEventStart":0.0,"ascentInMeters":39.4,"locKey":"LOC_ROUTE_PARIS_LUTECE_EXPRESS","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/PARIS_Lutece_Express.png","levelLocked":0,"publicEventsOnly":false,"supportedLaps":true,"leadinAscentInMeters":25.9,"leadinDistanceInMeters":3599.1,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]}]},{"name":"BOLOGNATT","routes":[{"name":"Bologna Time Trial","id":2843604888,"distanceInMeters":8050.0,"distanceInMetersFromEventStart":0.0,"ascentInMeters":236.39133,"locKey":"LOC_ROUTE_BOLOGNATT_TIME_TRIAL_LAP","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/BOLOGNATT_Bologna_Time_Trial.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":false,"leadinAscentInMeters":0.0,"leadinDistanceInMeters":0.0,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]}]}],"schedules":[{"map":"YORKSHIRE","start":"2022-01-01T04:01:00Z"},{"map":"FRANCE","start":"2022-01-03T04:01:00Z"},{"map":"RICHMOND","start":"2022-01-04T04:01:00Z"},{"map":"INNSBRUCK","start":"2022-01-07T04:01:00Z"},{"map":"MAKURIISLANDS","start":"2022-01-08T04:01:00Z"},{"map":"LONDON","start":"2022-01-11T04:01:00Z"},{"map":"YORKSHIRE","start":"2022-01-14T04:01:00Z"},{"map":"RICHMOND","start":"2022-01-15T04:01:00Z"},{"map":"FRANCE","start":"2022-01-16T04:01:00Z"},{"map":"MAKURIISLANDS","start":"2022-01-18T04:01:00Z"},{"map":"INNSBRUCK","start":"2022-01-21T04:01:00Z"},{"map":"YORKSHIRE","start":"2022-01-22T04:01:00Z"},{"map":"LONDON","start":"2022-01-23T04:01:00Z"},{"map":"RICHMOND","start":"2022-01-25T04:01:00Z"},{"map":"FRANCE","start":"2022-01-28T04:01:00Z"},{"map":"INNSBRUCK","start":"2022-01-30T04:01:00Z"},{"map":"RICHMOND","start":"2022-02-01T04:01:00Z"},{"map":"FRANCE","start":"2022-02-04T04:01:00Z"},{"map":"YORKSHIRE","start":"2022-02-06T04:01:00Z"},{"map":"LONDON","start":"2022-02-08T04:01:00Z"},{"map":"RICHMOND","start":"2022-02-11T04:01:00Z"},{"map":"MAKURIISLANDS","start":"2022-02-12T04:01:00Z"},{"map":"INNSBRUCK","start":"2022-02-14T04:01:00Z"},{"map":"MAKURIISLANDS","start":"2022-02-15T04:01:00Z"},{"map":"LONDON","start":"2022-02-18T04:01:00Z"},{"map":"YORKSHIRE","start":"2022-02-19T04:01:00Z"},{"map":"FRANCE","start":"2022-02-20T04:01:00Z"},{"map":"MAKURIISLANDS","start":"2022-02-22T04:01:00Z"},{"map":"INNSBRUCK","start":"2022-02-25T04:01:00Z"},{"map":"LONDON","start":"2022-02-26T04:01:00Z"},{"map":"RICHMOND","start":"2022-02-28T04:01:00Z"},{"map":"MAKURIISLANDS","start":"2022-03-01T04:01:00Z"},{"map":"FRANCE","start":"2022-03-04T04:01:00Z"},{"map":"INNSBRUCK","start":"2022-03-06T04:01:00Z"},{"map":"LONDON","start":"2022-03-08T04:01:00Z"},{"map":"YORKSHIRE","start":"2022-03-10T04:01:00Z"},{"map":"FRANCE","start":"2022-03-11T04:01:00Z"},{"map":"RICHMOND","start":"2022-03-13T04:01:00Z"},{"map":"MAKURIISLANDS","start":"2022-03-15T04:01:00Z"},{"map":"YORKSHIRE","start":"2022-03-18T04:01:00Z"},{"map":"FRANCE","start":"2022-03-20T04:01:00Z"},{"map":"RICHMOND","start":"2022-03-22T04:01:00Z"},{"map":"INNSBRUCK","start":"2022-03-25T04:01:00Z"},{"map":"MAKURIISLANDS","start":"2022-03-27T04:01:00Z"},{"map":"LONDON","start":"2022-03-29T04:01:00Z"}],"achievements":[{"id":1,"name":"JELLY","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/500w10sec.png"},{"id":2,"name":"MASTER DRAFTSMAN","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/Drafting.png"},{"id":3,"name":"WHOA NELLY","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/30mphTrike.png"},{"id":4,"name":"SPEED DEMON","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/40mphBike.png"},{"id":5,"name":"DAREDEVIL","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/50mphRocket.png"},{"id":6,"name":"CAN'T STOP NOW","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/Distance10.png"},{"id":8,"name":"SPRINTER APPRENTICE","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/Watt1.png"},{"id":9,"name":"LIT","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/Watt2.png"},{"id":10,"name":"CIRCUIT BREAKER","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/Watt3.png"},{"id":11,"name":"JUST SCRAPE IT","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/Watt4.png"},{"id":12,"name":"THE BLOWDRIER","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/Watt5.png"},{"id":13,"name":"PREMIER POWER","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/Watt6.png"},{"id":14,"name":"OFF THE ROCKS","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/Watt7.png"},{"id":15,"name":"1.21 GIGAWATTS","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/Watt8.png"},{"id":16,"name":"MARATHONER","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/Distance40km.png"},{"id":17,"name":"100 CLICKS","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/Distance100km.png"},{"id":18,"name":"NO BIG DEAL","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/Distance100mi.png"},{"id":22,"name":"HABITUAL","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/Ride3days.png"},{"id":23,"name":"ADDICTED","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/Ride7Days.png"},{"id":24,"name":"WORKING FROM HOME","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/Ride14days.png"},{"id":25,"name":"YOU'RE POPULAR","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/GetRideOn1.png"},{"id":26,"name":"YOU'RE FAMOUS","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/GetRideOn2.png"},{"id":27,"name":"BIGGER THAN JENSIE!","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/GetRideOn3.png"},{"id":28,"name":"PAPARAZZI","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/Fanview.png"},{"id":29,"name":"INTO THE WIND","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/Uturn.png"},{"id":30,"name":"RIDE ON","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/GiveRideOn1.png"},{"id":31,"name":"BIG FAN","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/GiveRideOn2.png"},{"id":32,"name":"FAN CLUB","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/GiveRideOn3.png"},{"id":34,"name":"STATISTICIAN","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/ConnectToStrava.png"},{"id":35,"name":"PAIRED","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/PairZwift.png"},{"id":37,"name":"SWEAT!","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/CompleteWorkout.png"},{"id":38,"name":"100KPH!","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/100kph.png"},{"id":39,"name":"WARMED UP","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/VolcanoLap01.png"},{"id":40,"name":"HOTHEAD","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/VolcanoLap02.png"},{"id":41,"name":"ON FIRE!","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/VolcanoLap03.png"},{"id":42,"name":"HOT OFF THE LINE","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/SessionRun1mi.png"},{"id":43,"name":"FIRST FIVE","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/SessionRun5k.png"},{"id":44,"name":"GIMMIE TEN","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/SessionRun10k.png"},{"id":45,"name":"RUNNERS DOZEN","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/SessionRun13mi.png"},{"id":46,"name":"GOING THE DISTANCE","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/SessionRun26mi.png"},{"id":47,"name":"CENTURION","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/DistanceRun100.png"},{"id":48,"name":"STREET CRED","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/DistanceRun500.png"},{"id":49,"name":"PURSUIT OF HAPPINESS","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/DistanceRun1000.png"},{"id":50,"name":"EARNING THE DONUTS","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/MileRun9min.png"},{"id":51,"name":"LEG WARMER","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/MileRun8min.png"},{"id":52,"name":"LIKE THE WIND","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/MileRun7min.png"},{"id":53,"name":"ENGINES ARE GO","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/MileRun6min.png"},{"id":54,"name":"OLYMPIAN","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/MileRun5min.png"},{"id":55,"name":"EVERESTED!","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/EverestChallenge.png"},{"id":56,"name":"AVID CLIMBER","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/ClimbAlpe5x.png"},{"id":57,"name":"MASOCHIST","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/ClimbAlpe25x.png"},{"id":58,"name":"LIFTOFF!","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/ClimbAlpe1hour.png"},{"id":59,"name":"2018 UCI WORLDS SHORT LAP","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":60,"name":"INNSBRUCKRING","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":61,"name":"ACHTERBAHN","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":62,"name":"LONDON LOOP","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":63,"name":"GREATER LONDON FLAT","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":64,"name":"LONDON CLASSIQUE","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":65,"name":"LONDON 8","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":66,"name":"SURREY HILLS","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":67,"name":"PARK PERIMETER LOOP","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":68,"name":"KNICKERBOCKER","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":69,"name":"ASTORIA LINE 8","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":70,"name":"EVERYTHING BAGEL","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":71,"name":"GRAND CENTRAL CIRCUIT","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":72,"name":"2015 UCI WORLDS COURSE","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":73,"name":"COBBLED CLIMBS","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":74,"name":"THE FAN FLATS","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":75,"name":"HILLY ROUTE","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":76,"name":"SAND AND SEQUOIAS","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":77,"name":"VOLCANO CIRCUIT","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":78,"name":"VOLCANO CLIMB","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":79,"name":"THREE SISTERS","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":80,"name":"2019 UCI WORLDS HARROGATE CIRCUIT","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":81,"name":"QUEEN'S HIGHWAY","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":82,"name":"DUCHY ESTATE","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":83,"name":"ROYAL PUMP ROOM 8","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":84,"name":"THE PRETZEL","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":85,"name":"JUNGLE CIRCUIT","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":86,"name":"ROAD TO SKY","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":87,"name":"FOUR HORSEMEN","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":88,"name":"TOUR OF FIRE AND ICE","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":89,"name":"BIG LOOP","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":90,"name":"MOUNTAIN ROUTE","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":91,"name":"MOUNTAIN 8","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":92,"name":"FIGURE 8","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":93,"name":"FIGURE 8 REVERSE","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":94,"name":"FLAT ROUTE","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":95,"name":"ROAD TO RUINS","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":96,"name":"THE MEGA PRETZEL","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":97,"name":"VOLCANO CIRCUIT CCW","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":98,"name":"VOLCANO FLAT","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":99,"name":"OUT AND BACK AGAIN","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":100,"name":"TEMPUS FUGIT","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":101,"name":"THE UBER PRETZEL","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":102,"name":"BIGGER LOOP","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":103,"name":"TICK TOCK","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":104,"name":"WHOLE LOTTA LAVA","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":105,"name":"MUIR AND THE MOUNTAIN","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":106,"name":"BIG FOOT HILLS","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":107,"name":"DUST IN THE WIND","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":108,"name":"QUATCH QUEST","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":109,"name":"GREATER LONDON 8","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":110,"name":"THE LONDON PRETZEL","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":111,"name":"THE PRL HALF","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":112,"name":"GREATEST LONDON FLAT","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":113,"name":"THE PRL FULL","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":114,"name":"GREATER LONDON LOOP","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":115,"name":"GREATEST LONDON LOOP","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":116,"name":"TRIPLE LOOPS","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":117,"name":"KNICKERBOCKER REVERSE","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":118,"name":"LADY LIBERTY","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":119,"name":"MIGHTY METROPOLITAN","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":120,"name":"RISING EMPIRE","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":121,"name":"THE 6 TRAIN","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":122,"name":"THE HIGHLINE","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":123,"name":"LUTSCHER","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":124,"name":"LUTSCHER CCW","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":125,"name":"TOUR OF TEWIT WELL","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":126,"name":"11.1 OCEAN BLVD","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":127,"name":"5K LOOP","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":128,"name":"CHILI PEPPER","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":129,"name":"JON'S ROUTE","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":130,"name":"THAT'S AMORE","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":131,"name":"COUCH TO SKY K","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":132,"name":"FLAT IRONS","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":133,"name":"HUDSON ROLL","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":134,"name":"PARK TO PEAK","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":135,"name":"SHUMAN TRAIL LOOP","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":136,"name":"MAY FIELD","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":137,"name":"R.G.V.","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":138,"name":"DOUCE FRANCE","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":139,"name":"CASSE PATTES","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":140,"name":"ROULE MA POULE","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":141,"name":"PETIT BOUCLE","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":142,"name":"TIRE BOUCHON","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":143,"name":"VEN-TOP","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":144,"name":"CHAMPS-ÉLYSÉES","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":145,"name":"LUTECE EXPRESS","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":146,"name":"BEACH ISLAND LOOP","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":147,"name":"OCEAN LAVA CLIFFSIDE LOOP","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":148,"name":"SERPENTINE 8","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":149,"name":"TWO BRIDGES LOOP","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":150,"name":"SEA TO TREE","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":151,"name":"KAPPA QUEST","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":152,"name":"CHAIN CHOMPER","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":153,"name":"COUNTRYSIDE TOUR","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":154,"name":"FLATLAND LOOP","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":155,"name":"TWO VILLAGE LOOP","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":156,"name":"SPIRIT FOREST","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":157,"name":"THREE VILLAGE LOOP","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":158,"name":"KAPPA QUEST REVERSE","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":159,"name":"SUKI'S PLAYGROUND","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":160,"name":"VALLEY TO MOUNTAINTOP","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":161,"name":"FARMLAND LOOP","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":162,"name":"CLIMBER'S GAMBIT","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":163,"name":"LEGENDS AND LAVA","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":164,"name":"RAILWAYS AND ROOFTOPS","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":165,"name":"ROOFTOP RENDEZVOUS","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":166,"name":"NEON FLATS","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":167,"name":"SPRINTER'S PLAYGROUND","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":168,"name":"NEOKYO ALL-NIGHTER","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":169,"name":"SLEEPLESS CITY","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":170,"name":"WANDERING FLATS","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":171,"name":"TEMPLES AND TOWERS","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":172,"name":"TWILIGHT HARBOR","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":173,"name":"CHASING THE SUN","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":174,"name":"ELECTRIC LOOP","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":175,"name":"CASTLE TO CASTLE","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":176,"name":"NEOKYO CRIT COURSE","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":177,"name":"THREE LITTLE SISTERS","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":178,"name":"TRIPLE FLAT LOOPS","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":179,"name":"DOWNTOWN TITANS","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":180,"name":"EASTERN EIGHT","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":181,"name":"MAYAN BRIDGE LOOP","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":182,"name":"ITALIAN VILLAS CIRCUIT","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":183,"name":"HANDFUL OF GRAVEL (CYCLING)","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":184,"name":"HANDFUL OF GRAVEL (RUNNING)","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"}],"unlockableCategories":[{"id":0,"name":"CYCLING_BIKE","imageUrl":"https://cdn.zwift.com/static/zc/UNLOCKABLE_CATEGORIES/CYCLING_BIKE.png"},{"id":1,"name":"CYCLING_JERSEY","imageUrl":"https://cdn.zwift.com/static/zc/UNLOCKABLE_CATEGORIES/CYCLING_JERSEY.png"},{"id":2,"name":"CYCLING_WHEELS","imageUrl":"https://cdn.zwift.com/static/zc/UNLOCKABLE_CATEGORIES/CYCLING_WHEELS.png"},{"id":3,"name":"CYCLING_PAINTJOB","imageUrl":"https://cdn.zwift.com/static/zc/UNLOCKABLE_CATEGORIES/CYCLING_PAINTJOB.png"},{"id":4,"name":"CYCLING_HEADGEAR","imageUrl":"https://cdn.zwift.com/static/zc/UNLOCKABLE_CATEGORIES/CYCLING_HEADGEAR.png"},{"id":5,"name":"CYCLING_SHOES","imageUrl":"https://cdn.zwift.com/static/zc/UNLOCKABLE_CATEGORIES/CYCLING_SHOES.png"},{"id":6,"name":"CYCLING_GLOVES","imageUrl":"https://cdn.zwift.com/static/zc/UNLOCKABLE_CATEGORIES/CYCLING_GLOVES.png"},{"id":7,"name":"RUNNING_HEADGEAR","imageUrl":"https://cdn.zwift.com/static/zc/UNLOCKABLE_CATEGORIES/RUNNING_HEADGEAR.png"},{"id":8,"name":"RUNNING_SHOES","imageUrl":"https://cdn.zwift.com/static/zc/UNLOCKABLE_CATEGORIES/RUNNING_SHOES.png"},{"id":9,"name":"RUNNING_KIT","imageUrl":"https://cdn.zwift.com/static/zc/UNLOCKABLE_CATEGORIES/RUNNING_KIT.png"},{"id":10,"name":"SOCKS","imageUrl":"https://cdn.zwift.com/static/zc/UNLOCKABLE_CATEGORIES/SOCKS.png"},{"id":11,"name":"EYEWEAR","imageUrl":"https://cdn.zwift.com/static/zc/UNLOCKABLE_CATEGORIES/EYEWEAR.png"}],"missions":[],"challenges":[{"id":1231,"name":"CLIMB MT.EVEREST","imageUrl":"https://cdn.zwift.com/static/zc/CHALLENGES/CLIMB_MT_EVEREST.png"},{"id":1234153,"name":"RIDE CALIFORNIA","imageUrl":"https://cdn.zwift.com/static/zc/CHALLENGES/RIDE_CALIFORNIA.png"},{"id":15313453,"name":"TOUR ITALY","imageUrl":"https://cdn.zwift.com/static/zc/CHALLENGES/TOUR_ITALY.png"}],"jerseys":[{"id":3523119,"name":"TBR 2020","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TBR2020_thumb.png"},{"id":27953592,"name":"Off the Maap 2021 Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/OffTheMaap2021_thumb.png"},{"id":55762178,"name":"WEDU Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/WeduOG2021_thumb.png"},{"id":63314504,"name":"Ride Australia","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/RideAustralia_thumb.png"},{"id":67952083,"name":"ZNC - Netherlands","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftNationalChampions_thumb.png"},{"id":69260263,"name":"Sunweb","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/GiantAlpecin_thumb.png"},{"id":83047882,"name":"Crohn's & Colitis Foundation","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/CrohnsColitis2021_thumb.png"},{"id":84221876,"name":"WTRL","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/WTRL2020_thumb.png"},{"id":85605155,"name":"Spain Elite","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/SpainElite2020_thumb.png"},{"id":90433914,"name":"Zwift Standard Orange","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftStandardOrange_thumb.png"},{"id":91616735,"name":"PAC","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/PAC_thumb.png"},{"id":97396552,"name":"Team CLS 2019","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/CLS2019_thumb.png"},{"id":97975537,"name":"Ceratizit-WNT","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/WNTRotor2019_thumb.png"},{"id":120279066,"name":"Super League Community 2021 Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/SuperLeagueCommunity2021_thumb.png"},{"id":121390527,"name":"Tour Of Watopia","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TourOfWatopia2019_thumb.png"},{"id":122407386,"name":"Specialized Bicycledelics Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/SpecializedBicycledelics2021_thumb.png"},{"id":123854412,"name":"Alpine Slopes 1","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_Ski01_thumb.png"},{"id":139090872,"name":"BMTR Community Club","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BearMountaineers2021_thumb.png"},{"id":141416810,"name":"Collins Cup Europe","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/CollinsCupBlue2020_thumb.png"},{"id":142676981,"name":"Assos Superleger","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/AssosSuperleger2021_thumb.png"},{"id":153074962,"name":"Finesse 2021","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Finesse2021_thumb.png"},{"id":158338255,"name":"SZR","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/SZR_thumb.png"},{"id":167026782,"name":"Maratona Dolomiti","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/MaratonaDolomiti_thumb.png"},{"id":172691656,"name":"Castelli Virtual Aero Light","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/CastelliVirtualClimber_Light_thumb.png"},{"id":173668358,"name":"Colombia Elite","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ColombiaFederation2020_thumb.png"},{"id":179248736,"name":"Echelon-Cannondale-4iiii","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/EchelonCannondale2021_thumb.png"},{"id":179750023,"name":"Toyota","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Toyota2020_thumb.png"},{"id":188440276,"name":"Z Fondo February","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZFondo2020Feb_thumb.png"},{"id":189587516,"name":"CANYON//SRAM Generation","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/CanyonSRAMGen2022_thumb.png"},{"id":197716012,"name":"Wattbike","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Wattbike_thumb.png"},{"id":204407447,"name":"zFondo November 2020","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZFondo2020Nov_thumb.png"},{"id":207969420,"name":"ZNC - Belgium","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftNationalChampions_thumb.png"},{"id":215771303,"name":"NTT","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/NTT2022_thumb.png"},{"id":216011959,"name":"2021 Neokyo Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Neokyo2021_thumb.png"},{"id":225381880,"name":"Pinarello Dogma","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/PinarelloDogma_thumb.png"},{"id":230859106,"name":"ATP Racing 2021","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ATPRacing2021_thumb.png"},{"id":239397429,"name":"Adicta Lab BMC SLR","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/AdictaSLR2022_thumb.png"},{"id":243478565,"name":"Powertap","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Powertap_thumb.png"},{"id":246267937,"name":"Sprint","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_Green_Jersey_Female.png"},{"id":247987096,"name":"Level 20","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_LVL20_Jersey_thumb.png"},{"id":248516205,"name":"Ride On","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/RideOnJersey_thumb.png"},{"id":253887160,"name":"Zwift Race Leader","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_RaceLeader_thumb.png"},{"id":257830838,"name":"Novator","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Novator2021_thumb.png"},{"id":257930879,"name":"Cycle-Smart","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/CycleSmart2021_thumb.png"},{"id":273275384,"name":"Continental","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Continental2018_thumb.png"},{"id":281027822,"name":"Dempsey Challenge","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/DempseyChallenge2020_thumb.png"},{"id":292465293,"name":"GCN","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/GCN_2017_thumb.png"},{"id":298968645,"name":"VW","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/VW_2015_thumb.png"},{"id":303167268,"name":"Cadex Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Cadex2021_thumb.png"},{"id":304856285,"name":"Lava","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Lava2015_thumb.png"},{"id":307186134,"name":"Orica Scott","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/OricaScott_thumb.png"},{"id":315634460,"name":"Saris-NoPinz","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/SarisNoPinz2022_thumb.png"},{"id":321508751,"name":"Bora-Hansgrohe","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BoraHansgrohe2018_thumb.png"},{"id":330017973,"name":"GGCC","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/GGCC2020_thumb.png"},{"id":351896555,"name":"Zwift","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_02_thumb.png"},{"id":352543030,"name":"Mitchelton-SCOTT","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/MitcheltonScottPro2019_thumb.png"},{"id":354409479,"name":"ZWC 2021","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZWC2021_thumb.png"},{"id":360667535,"name":"Andy Schleck Cappuccino Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/AndySchleck2021_thumb.png"},{"id":360956826,"name":"La Fuga","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/LaFuga2015_thumb.png"},{"id":363655187,"name":"Basic 1","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Plain_01_thumb.png"},{"id":364387115,"name":"Garmin Never Stop Cycling","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Garmin2020_thumb.png"},{"id":374392241,"name":"Peaks Coaching Group","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/PeaksCoachingGroup_thumb.png"},{"id":384005997,"name":"Eliel","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Eliel2021_thumb.png"},{"id":405683158,"name":"Tower 26 Triathlon","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Tower262021_thumb.png"},{"id":407278237,"name":"ZNC - South Africa","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftNationalChampions_thumb.png"},{"id":407394860,"name":"HUUB","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/HUUB2019_thumb.png"},{"id":409344255,"name":"Radavist","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Radavist_thumb.png"},{"id":410321313,"name":"CHPT3","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Chapter32022_thumb.png"},{"id":410869722,"name":"Zwift Academy Tri 2018","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftAcademyTri2018_thumb.png"},{"id":411320964,"name":"Fluoro 3","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftFluoro03_thumb.png"},{"id":413159800,"name":"PRSC 2019","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/PRSC2019_thumb.png"},{"id":415011426,"name":"Vintage 3","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Plain_08_thumb.png"},{"id":418778055,"name":"AHDR Giant","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/AHDRGiant_thumb.png"},{"id":425798382,"name":"Norseman Black Jersey","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Norseman2018_thumb.png"},{"id":429292215,"name":"Adventure Stache 2020","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/AdventureStache2020_thumb.png"},{"id":434724571,"name":"Tri 247","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Tri2472019_thumb.png"},{"id":435322285,"name":"BC National Champion","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BCNationalChampion2019_thumb.png"},{"id":438222824,"name":"Team Dauner","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TeamDauner2021_thumb.png"},{"id":442921983,"name":"Training Peaks","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TrainingPeaks_thumb.png"},{"id":448644816,"name":"Zwift Academy 2020 Womens","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftAcademyWomen2020_thumb.png"},{"id":459579137,"name":"4iiii","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/4iii_thumb.png"},{"id":462436923,"name":"Docomo","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Docomo_thumb.png"},{"id":464353534,"name":"Team LCB Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/LucyCharles2018_thumb.png"},{"id":480846458,"name":"Thomson Bike Tours","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Thomson2021_thumb.png"},{"id":493134166,"name":"Team 3R","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Team3R2018_thumb.png"},{"id":501435498,"name":"Super League Tri Arena Games - White Jersey","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/SLT2022white_thumb.png"},{"id":513440680,"name":"Saxo","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Saxo_thumb.png"},{"id":520081294,"name":"Clash Of Clubs Blue","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ClashOfClubs2020_thumb.png"},{"id":528534981,"name":"Evo Cycling Club 2020","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/EvoCyclingClub2020_thumb.png"},{"id":532913371,"name":"Sky","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Sky_2014_thumb.png"},{"id":537047233,"name":"Alé BTC Ljubljana","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/AleCipollini2020_thumb.png"},{"id":542207259,"name":"INEOS Grenadiers 2022 Pro","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/INEOSGrenadiers2021_thumb.png"},{"id":552170906,"name":"Israel Premier-Tech","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/IsraelPT2022_thumb.png"},{"id":555613191,"name":"DD-Qhubeka-Fan","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/DD-Qhubeka-Fan_thumb.png"},{"id":571847813,"name":"Giant Seorak Gran Fondo","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/GiantSeorakGranFondo_thumb.png"},{"id":579376079,"name":"German Cycling Academy","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/GermanCyclingAcademy2019_thumb.png"},{"id":581094550,"name":"Prudential Ride London","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/PrudentialRideLondon2019_thumb.png"},{"id":594066491,"name":"Bo Bikes Bama","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BoBikesBama2020_thumb.png"},{"id":596258669,"name":"Tour Of New York","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TourOfNy_thumb.png"},{"id":598277887,"name":"GTCC for G. Thomas","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/GTCC2021_thumb.png"},{"id":598687666,"name":"Arkea-Samsic","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ArkeaSamsic2019_thumb.png"},{"id":601928732,"name":"Geumsan Insam Cello","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/GeumsanInsamCello_thumb.png"},{"id":605422273,"name":"Power Up","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/PowerUp2020_thumb.png"},{"id":608088435,"name":"Garneau 2018","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Garneau2018_thumb.png"},{"id":608333324,"name":"France Elite","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/FrenchFederationElite2020_thumb.png"},{"id":614640035,"name":"Super League Tri Arena Games - Green Jersey","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/SLT2022green_thumb.png"},{"id":622687850,"name":"Zwift Academy Tri","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftAcademyTri2019_thumb.png"},{"id":630051236,"name":"Guerra Vision","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/GuerraVision_thumb.png"},{"id":631173451,"name":"Team Italy","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TeamItaly2019_thumb.png"},{"id":635726644,"name":"Fluoro 2","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftFluoro02_thumb.png"},{"id":636410390,"name":"DBR Community 2022","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/DBRCommunity2022_thumb.png"},{"id":641629754,"name":"Madison Genesis Champ","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/MadisonGenesisNatChamp2019_thumb.png"},{"id":645291915,"name":"Vox Women","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/VoxWomen2020_thumb.png"},{"id":658499945,"name":"Team Wiggins 2018","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TeamWiggins2018_thumb.png"},{"id":664486855,"name":"TTR","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TRR2020_thumb.png"},{"id":672638644,"name":"Century","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_Century_Jersey_thumb.png"},{"id":673116471,"name":"Trinidad and Tobago Federation Elite 2022","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/UCIBlackTT2022_thumb.png"},{"id":677436092,"name":"Phil's Cookie Fondo Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/PhilGaimonCookie2021_thumb.png"},{"id":681866314,"name":"Vintage 1","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_Retro_01_thumb.png"},{"id":706154061,"name":"Eritrea Federation Elite 2022","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/UCIBlackEritrea2022_thumb.png"},{"id":713852384,"name":"Giant Ride Like King","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/RideLikeKing2019_thumb.png"},{"id":716189513,"name":"VCGH","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/VCGH_thumb.png"},{"id":732880966,"name":"Level 15","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_LVL15_Jersey_thumb.png"},{"id":733758360,"name":"Richie Carapaz Cycling Club","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/RichieCarapazCC2021_thumb.png"},{"id":750929824,"name":"Tour de Zwift 2022 Ride Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TourDeZwift2022_thumb.png"},{"id":751130480,"name":"Stages Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/StagesCycling2021_thumb.png"},{"id":752882810,"name":"Kissena","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Kissena2021_thumb.png"},{"id":766246587,"name":"Stages Cycling","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/StagesCycling_thumb.png"},{"id":772368494,"name":"Zwift Academy 2017","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftAcademyWomen2017_thumb.png"},{"id":773720424,"name":"AlienWare","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/AlienWare_thumb.png"},{"id":774250347,"name":"Norvator Green","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/NovatorGreen2021_thumb.png"},{"id":775444693,"name":"Rapha Womens 100 2021","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/RaphaWomens1002021_thumb.png"},{"id":799948511,"name":"Mark Allen Coaching","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/MarkAllenCoaching2018_thumb.png"},{"id":801304718,"name":"St Kilda CC","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/StKildaCC2015_thumb.png"},{"id":804522168,"name":"Team Fearless","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TeamFearless2019_thumb.png"},{"id":806962508,"name":"Castelli Ride with Reggie","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/RideReggie2021_thumb.png"},{"id":810188791,"name":"Bicycling 2017","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Bicycling2017_thumb.png"},{"id":814156923,"name":"MTS","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/MTS_thumb.png"},{"id":824066136,"name":"AHDR Europe","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/AHDR2021_thumb.png"},{"id":832135341,"name":"DIRT Community Club","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/DIRT2021_thumb.png"},{"id":835382123,"name":"Pearson CC","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/PearsonCC_thumb.png"},{"id":836121883,"name":"Singapore Federation Elite 2022","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/SingaporeUCI2022_thumb.png"},{"id":836965768,"name":"VeloNews","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/VeloNews_thumb.png"},{"id":841653846,"name":"Women's Week 2018","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_WomensWeek_thumb.png"},{"id":842129295,"name":"7-Eleven","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/7Eleven_thumb.png"},{"id":850907408,"name":"MAAP","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Maap2019_thumb.png"},{"id":854534852,"name":"Human Powered Health Fan","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/HPHFan2022_thumb.png"},{"id":858896296,"name":"British Cycling Member","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BritishCyclingMember_thumb.png"},{"id":866555848,"name":"Black Sheep","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BlackSheep2018_thumb.png"},{"id":875567104,"name":"Hincapie Racing","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/HincapieRacing2019_thumb.png"},{"id":876762852,"name":"ATOC Overall","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ATOC_Leader_2017.png"},{"id":881081205,"name":"Zwift zFondo March 2019","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftZFondoMar2019_thumb.png"},{"id":888463839,"name":"Zwift eFondo July","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_eFondo_Jul_thumb.png"},{"id":893312655,"name":"Pedal Racing 2018","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/PedalRacing2018_thumb.png"},{"id":901269715,"name":"Black Sheep Blue","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BlackSheepOrange2019_thumb.png"},{"id":901886770,"name":"eLoveTiles","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ELoveTiles2022_thumb.png"},{"id":902756936,"name":"Zwift eFondo September","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_eFondo_Sept_thumb.png"},{"id":908866226,"name":"DD UK Champ","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/DimensionDataUKChamp_thumb.png"},{"id":920337189,"name":"Bartali","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Bartali2020_thumb.png"},{"id":927604154,"name":"Cofidis 2018","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Cofidis2018_thumb.png"},{"id":933043223,"name":"Goseong Gran Fondo","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/GoseongGranFondo2018_thumb.png"},{"id":933293927,"name":"Tour Of Innsbruck","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TourOfInnsbruck2018_thumb.png"},{"id":947106687,"name":"Ireland Elite","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/IrelandElite2020_thumb.png"},{"id":949542640,"name":"Zipp","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zipp2019_thumb.png"},{"id":955816145,"name":"Tour of Watopia 2018","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_TourOfWatopia2018_thumb.png"},{"id":958209686,"name":"Haute Route x Zwift","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/HauteRoutexZwift2020_thumb.png"},{"id":958696823,"name":"Team Jumbo-Visma Men","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/JumboVismaMen2022_thumb.png"},{"id":971980441,"name":"Bicycling","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Bicycling_thumb.png"},{"id":980654946,"name":"Deloitte Digital","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/DeloitteDigital_thumb.png"},{"id":981392121,"name":"Alpecin-Fenix World Champion 2022","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/AlpecinFenixWC2022_thumb.png"},{"id":988976680,"name":"British Cycling","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BritishCycling_thumb.png"},{"id":989955139,"name":"Team Experimental","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TeamExperimental_thumb.png"},{"id":996458198,"name":"Rapha Flyweight","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/RaphaFlyweight_thumb.png"},{"id":1000257656,"name":"Rocacorba","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Rocacorba2021_thumb.png"},{"id":1021172827,"name":"Hincapie","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Hincapie2020_thumb.png"},{"id":1022121143,"name":"Castelli Virtual Aero Green","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/CastelliVirtualAero_Green_thumb.png"},{"id":1022287351,"name":"TIBCO-Silicon Valley Bank","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Tibco2020_thumb.png"},{"id":1025753934,"name":"Fight ALS","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/AndreGreipel2019_thumb.png"},{"id":1028356106,"name":"AHDR Giant Bison","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/AHDRGiantBison_thumb.png"},{"id":1033576880,"name":"Sub 8","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Sub82022_thumb.png"},{"id":1040478737,"name":"2XU Cycling","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/2XUCycling2021_thumb.png"},{"id":1055383332,"name":"zFondo Mar 2021","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZFondo2021Mar_thumb.png"},{"id":1065184178,"name":"CA Technologies 2018","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/CaTech2018_thumb.png"},{"id":1066013543,"name":"Eric Min T Day Jersey","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/EricMinTDay2021_thumb.png"},{"id":1067526784,"name":"ZHR","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZHR2022_thumb.png"},{"id":1072467839,"name":"Giant Ride Like King 2020","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/RideLikeKing2020_thumb.png"},{"id":1075174482,"name":"TS-Bikes","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TS-Bikes_thumb.png"},{"id":1075430387,"name":"SZ","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/SZ2020_thumb.png"},{"id":1086432412,"name":"Alpine Slopes 2","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_Ski02_thumb.png"},{"id":1087938762,"name":"Tour For All","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TourForAll2020_thumb.png"},{"id":1088165651,"name":"Collins Cup USA","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/CollinsCupRed2020_thumb.png"},{"id":1092846644,"name":"Zwift eFondo August","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_eFondo_Aug_thumb.png"},{"id":1097944869,"name":"Computrainer","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Computrainer_thumb.png"},{"id":1126295234,"name":"Specialized F","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/SpecializedDolmansSLPro_thumb.png"},{"id":1128201030,"name":"Arkea","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ArkeaWomens2020_thumb.png"},{"id":1137000404,"name":"Espresso Cycle & Tri Coaching","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Espresso2022_thumb.png"},{"id":1139642427,"name":"ZNC - Switzerland","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftNationalChampions_thumb.png"},{"id":1139853189,"name":"Specialized Mix Tape","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/SpecializedMixTape2019_thumb.png"},{"id":1154847422,"name":"Trek-Segafredo Women","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TrekWomen2020_thumb.png"},{"id":1155032025,"name":"Specialized SL Air Fade","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/SpecializedAirFade2021_thumb.png"},{"id":1160098954,"name":"Clash Of Clubs Orange","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ClashOfClubs2020_thumb.png"},{"id":1160739978,"name":"Mitchelton Scott Daryl Impey","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/MitcheltonScottDarylImpey2019_thumb.png"},{"id":1162075523,"name":"ZNC - New Zealand","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftNationalChampions_thumb.png"},{"id":1164206060,"name":"Hotchillee Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Hotchillee2021_thumb.png"},{"id":1172586482,"name":"Norway Elite","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/NorwayElite2020_thumb.png"},{"id":1176516281,"name":"Axeon Cycling Team","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Axeon_thumb.png"},{"id":1178553774,"name":"Peta-Z Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/PetaZ2021_thumb.png"},{"id":1195756793,"name":"SiS","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/SiS2018_thumb.png"},{"id":1196271164,"name":"Velonews Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/VeloNews_thumb.png"},{"id":1196292138,"name":"Haute Route","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/HauteRoute3day_thumb.png"},{"id":1198454936,"name":"ZNC - Germany","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftNationalChampions_thumb.png"},{"id":1214754061,"name":"ATOC","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ATOC_2015_thumb.png"},{"id":1216459015,"name":"Alzheimers Association","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/AlzheimersAssociation2021_thumb.png"},{"id":1233478884,"name":"Mid Devon Cycling Club","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/MidDevonCC_thumb.png"},{"id":1236323268,"name":"Ribble Pro Cycling","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Ribble2019_thumb.png"},{"id":1247156896,"name":"Zwift zFondo September","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_zFondo2018_Sept_thumb.png"},{"id":1276763333,"name":"SAS","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/SASCycleClub_thumb.png"},{"id":1277618322,"name":"Liv Racing Xstra","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/LivRacingXstra2022_thumb.png"},{"id":1278086739,"name":"Dimension Data","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/DimensionData_thumb.png"},{"id":1278706482,"name":"Team Vikings","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TeamVikings2019_thumb.png"},{"id":1280314392,"name":"JetBlack 2020","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/JetBlack2020_thumb.png"},{"id":1283536276,"name":"SWE Cycling Elite","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/SWECyclingElite2019_thumb.png"},{"id":1287196397,"name":"Danish Cycling Member","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/DanishCyclingMember2019_thumb.png"},{"id":1287916199,"name":"Sprint/KOM","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_Green_KOM.png"},{"id":1294362827,"name":"Velosport","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Velosport2021_thumb.png"},{"id":1294918791,"name":"ZNC - Norway","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftNationalChampions_thumb.png"},{"id":1296192838,"name":"Hetis Koers","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/HetisKoers_thumb.png"},{"id":1296504749,"name":"Giro De Rigo","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/GiroDeRigo2019_thumb.png"},{"id":1301227383,"name":"LECOL","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/LECOL_thumb.png"},{"id":1303932596,"name":"Z Racing E","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZRacingE_thumb.png"},{"id":1305457056,"name":"Canyon Community","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/CanyonCommunity2019_thumb.png"},{"id":1306416237,"name":"Dutch Racing 2020","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/DutchRacing2020_thumb.png"},{"id":1309653199,"name":"Canyon Esports Devo 2020","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/CanyonEsportsDevo2020_thumb.png"},{"id":1312076479,"name":"Zwift eFondo June","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_eFondo_Jun_thumb.png"},{"id":1312480364,"name":"Wanty Gobert","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/WantyGobert2021_thumb.png"},{"id":1314892694,"name":"Super League Tri Arena Games - Blue Jersey","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/SLT2022blue_thumb.png"},{"id":1324536719,"name":"Finland Federation Elite 2022","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/FinlandUCI2022_thumb.png"},{"id":1327508500,"name":"Parlee","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Parlee2020_thumb.png"},{"id":1331639420,"name":"DD Cavendish","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/DimensionData_thumb.png"},{"id":1338967652,"name":"Trainsharp","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Trainsharp_thumb.png"},{"id":1346889258,"name":"BMC","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BMC_2014_thumb.png"},{"id":1370848124,"name":"Zwift Academy Men","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftAcademyMen2019_thumb.png"},{"id":1376553667,"name":"Vintage 2","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Plain_02_thumb.png"},{"id":1383009663,"name":"Dempsey Challenge 2021","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/DempeyChallenge2021_thumb.png"},{"id":1384420643,"name":"Wahoo Custom","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/WahooCustom2018_thumb.png"},{"id":1391024631,"name":"Performance Bike","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/PerformanceBike2018_thumb.png"},{"id":1398033027,"name":"Kirchmair 2021","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Kirchmair2021_thumb.png"},{"id":1398405435,"name":"Zwift Beta","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_01_thumb.png"},{"id":1405652749,"name":"Team Novo Nordisk 2019","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TeamNovoNordisk2019_thumb.png"},{"id":1413575650,"name":"Tour Of London","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TourOfLondon2018_thumb.png"},{"id":1415061663,"name":"UHC","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/UHC_thumb.png"},{"id":1420135286,"name":"CHPT3 Women","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/CHPT3Women2022_thumb.png"},{"id":1423767803,"name":"Lotto Soudal Ladies","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/LottoSoudalWomens_thumb.png"},{"id":1433773840,"name":"HDR","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/HDR_thumb.png"},{"id":1442976960,"name":"Tour De Zwift","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TourDeZwift2020_thumb.png"},{"id":1444876828,"name":"Jensie Gran Fondo","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/JensieGranFondo2015_thumb.png"},{"id":1446002244,"name":"Lamont Cycling","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/LamontCycling2019_thumb.png"},{"id":1456211971,"name":"Circus - Wanty Gobert","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/CircusWantyGobert_thumb.png"},{"id":1461334436,"name":"ATOC 2018","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ATOC2018_thumb.png"},{"id":1464573843,"name":"Zwift zFondo January 2019","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftZFondoJan2019_thumb.png"},{"id":1482458261,"name":"Morning Glory CC","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/MorningGloryCC_thumb.png"},{"id":1482663746,"name":"Saint Augustine's University Cycling","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/SAUCycling2022_thumb.png"},{"id":1485473947,"name":"Geelong CC","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/GeelongClub_thumb.png"},{"id":1494912035,"name":"Zwift Academy Tri 2020","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftAcademyTriCycle2020_thumb.png"},{"id":1499244597,"name":"Wahoo Le CoL Team Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/WahooLeCoL2021_thumb.png"},{"id":1502842919,"name":"Zwift Allstars","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftAllstars2019_thumb.png"},{"id":1505480275,"name":"B&B Hotels - Vital Concept p/b KTM","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/VitalConcept2020_thumb.png"},{"id":1513854402,"name":"CycleVox","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/CycleVox_thumb.png"},{"id":1518198854,"name":"Herd Cycling 2020","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/HerdCycling2020_thumb.png"},{"id":1530790011,"name":"Olivers Racing","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/OliversRacing2019_thumb.png"},{"id":1533880694,"name":"Life time","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/LifeCycle2021_thumb.png"},{"id":1541349594,"name":"Team Jumbo Visma-Women","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/JumboVismaWomen2022_thumb.png"},{"id":1561591671,"name":"Bolt Race Team Hellcatz","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BRTWomens2019_thumb.png"},{"id":1567544184,"name":"La Grange","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/LaGrange2019_thumb.png"},{"id":1582340905,"name":"zFondo Jan 2021","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZFondo2021Jan_thumb.png"},{"id":1587982785,"name":"AG2R La Mondiale","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/AG2R2020_thumb.png"},{"id":1589345470,"name":"BIGLA UCI","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BIGLA2019UCI_thumb.png"},{"id":1599435973,"name":"ZNC - Canada","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftNationalChampions_thumb.png"},{"id":1599692741,"name":"Tour of Watopia Finishers Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TourOfWatopiaRed2021_thumb.png"},{"id":1609094897,"name":"BC BR","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BCBR2020_thumb.png"},{"id":1617481982,"name":"Shimano","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Shimano2021_thumb.png"},{"id":1619804500,"name":"Machines Hive","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/FreedomMachine2019_thumb.png"},{"id":1622494578,"name":"Bike MS","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BikeMS2019_thumb.png"},{"id":1624079531,"name":"Epic KOM","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/KOM_Epic.png"},{"id":1624243839,"name":"SWE Cycling Member","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/SWECyclingMember2019_thumb.png"},{"id":1628085330,"name":"Tour de Pier","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TourDePier_2015_thumb.png"},{"id":1628258516,"name":"SEG Racing","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/SEGRacing2019_thumb.png"},{"id":1642410031,"name":"Prudential Ride London 2018","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/PrudentialRideLondon_thumb.png"},{"id":1643648851,"name":"QT2 Systems 2019","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/QT2Systems2019_thumb.png"},{"id":1647663707,"name":"Garmin Tacx Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/GarminTacx2021_thumb.png"},{"id":1648628196,"name":"Fluoro 1","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftFluoro01_thumb.png"},{"id":1650733257,"name":"Betty Designs Pink","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BettyDesignsPink_thumb.png"},{"id":1652462599,"name":"Next-Enshored Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/NextEnshored2021_thumb.png"},{"id":1677202555,"name":"Indoor Specialist","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/IndoorSpecialist_thumb.png"},{"id":1701380341,"name":"Assos Speed Club 2019","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/AssosSpeedClub2019_thumb.png"},{"id":1716914056,"name":"Cycling Weekly Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/CyclingWeekly2021_thumb.png"},{"id":1718052948,"name":"2021 ZA Tri Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZATri2021_thumb.png"},{"id":1727340017,"name":"Hong Kong Federation Elite 2022","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/UCIBlackHK2022_thumb.png"},{"id":1735340823,"name":"Level 40","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_LVL40_thumb.png"},{"id":1746959823,"name":"Skyline","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Skyline2020_thumb.png"},{"id":1751349769,"name":"UAE","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/UAE_2017_thumb.png"},{"id":1756517729,"name":"UnoXPro2022","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/UnoXPro2022_thumb.png"},{"id":1766102862,"name":"Elite","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Elite_thumb.png"},{"id":1776034995,"name":"Trek Segafredo Training","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TrekSegafredoTrain2022_thumb.png"},{"id":1801600368,"name":"Tour de Zwift 2021 Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TourDeZwift2021_thumb.png"},{"id":1805412330,"name":"Vini Fantini","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ViniFantini2019_thumb.png"},{"id":1808939337,"name":"Tour De Tucson","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TourDeTucson2020_thumb.png"},{"id":1809990475,"name":"Team Draft","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TeamDraft2019_thumb.png"},{"id":1812381670,"name":"Ineos Champ","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/IneosChamp2020_thumb.png"},{"id":1813617733,"name":"Tour of Watopia Starter Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TourOfWatopiaWhite2021_thumb.png"},{"id":1820653242,"name":"ZESP","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZESP2021_thumb.png"},{"id":1825919692,"name":"Zwift Academy Men 2018","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftAcademyMen2018_thumb.png"},{"id":1827481335,"name":"Korea Cup","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/KoreaCup2018_thumb.png"},{"id":1832490174,"name":"Overall","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_Orange_Jersey_Female.png"},{"id":1834190741,"name":"Black Cyclists Network","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BlackCyclistsNetwork2020_thumb.png"},{"id":1842355135,"name":"Movistar Team","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/MovistarTeam2022_thumb.png"},{"id":1851027443,"name":"Black Celebration Series Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BlackHistoryMonth2021_thumb.png"},{"id":1854542570,"name":"Wyn Republic","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/WynTri2020_thumb.png"},{"id":1857854426,"name":"Best Buddies International","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BestBuddies2021_thumb.png"},{"id":1869390707,"name":"Basic 2","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Plain_03_thumb.png"},{"id":1879012216,"name":"Cyclist","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/CyclistMag_thumb.png"},{"id":1884307159,"name":"ZNC - Poland","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftNationalChampions_thumb.png"},{"id":1893222148,"name":"Z Racing D","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZRacingD_thumb.png"},{"id":1900024720,"name":"ZNC - Korea","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftNationalChampions_thumb.png"},{"id":1900079957,"name":"BTCNJ","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BTCNJ_thumb.png"},{"id":1905817600,"name":"Innsbruck Fan Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/InnsbruckWorldChampionships2018_thumb.png"},{"id":1912822249,"name":"Endurance Nation","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/EnduranceNation2019_thumb.png"},{"id":1935761488,"name":"Team BikeExchange Women","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BikeExchangeWomen2022_thumb.png"},{"id":1936574954,"name":"ZBR","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZBR_thumb.png"},{"id":1939003542,"name":"Pan Mass Challenge","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/PanMass2021_thumb.png"},{"id":1947253559,"name":"AHDR Liv Bison","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/AHDRLivBison_thumb.png"},{"id":1957749265,"name":"Ruhr Riders","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftRuhrRiders2018_thumb.png"},{"id":1969335676,"name":"ASTANA PRO TEAM","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Astana2020_thumb.png"},{"id":1983748286,"name":"Le Col Legends 2021","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/LeColLegends2021_thumb.png"},{"id":1986777769,"name":"Madison Genesis","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/MadisonGenesis2018_thumb.png"},{"id":1987737590,"name":"ATOC 2017","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/AmgenTourCA_thumb.png"},{"id":1988029743,"name":"Rapha W100","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Rapha100Womens2020_thumb.png"},{"id":1995932006,"name":"Mt Fuji HC 2022","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/MTFUJIHC2022_thumb.png"},{"id":2007280751,"name":"CRCA","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/CRCA_thumb.png"},{"id":2010217529,"name":"Vegan Cyclist","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/VeganCyclist2019_thumb.png"},{"id":2021712776,"name":"Zwift zFondo November","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_zFondo_Nov_thumb.png"},{"id":2027807142,"name":"Pride On 2019","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/PrideOn2019_thumb.png"},{"id":2041039623,"name":"SD Worx","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TeamSDworx2021_thumb.png"},{"id":2050061293,"name":"British Triathlon Elite","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BrisithTriathlonElite2018_thumb.png"},{"id":2050259031,"name":"Scott","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Scott2020_thumb.png"},{"id":2053768111,"name":"Level 30","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_LVL30_thumb.png"},{"id":2087198235,"name":"Castelli Italia","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/SantiniItalia2017_thumb.png"},{"id":2087981349,"name":"To Be Determined","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ToBeDetermined2021_thumb.png"},{"id":2089349362,"name":"inGAMBA","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/inGamba_01_thumb.png"},{"id":2092402045,"name":"Total Direct Energie","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TotalDirectEnergie2020_thumb.png"},{"id":2099251947,"name":"Qhubeka Moving Forward 2020","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/QhubekaMovingForward2020_thumb.png"},{"id":2100122621,"name":"Tour De Oz","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TourDeOz2018_thumb.png"},{"id":2103755962,"name":"Slowtwitch","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Slowtwitch_thumb.png"},{"id":2107739436,"name":"Alpine Slopes 3","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_Ski03_thumb.png"},{"id":2114571944,"name":"Doltcini Van Eyck","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/DoltciniVanEyck.png"},{"id":2115002683,"name":"MTN-Qhubeka","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/MTN-Qhubeka_thumb.png"},{"id":2115369870,"name":"Rapha RCC","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/RaphaRCC2021_thumb.png"},{"id":2116459621,"name":"Drops","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/DropsWomens2020_thumb.png"},{"id":2124914476,"name":"L'Etape","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/LEtape_thumb.png"},{"id":2134531491,"name":"Team Vegan","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TeamVegan2020_thumb.png"},{"id":2140454859,"name":"Zwift zFondo February","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_zFondo_Feb_thumb.png"},{"id":2140478849,"name":"Trek-Segafredo Men","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Trek_2017_Red_thumb.png"},{"id":2150370418,"name":"Sugoi","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Sugoi2018_thumb.png"},{"id":2155858980,"name":"Bahrain McLaren","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BahrainMclaren2020_thumb.png"},{"id":2163356336,"name":"TdF Letape","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TdFLetape2020_thumb.png"},{"id":2174450522,"name":"Chicks Who Ride Bikes","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ChicksRideBikes2021_thumb.png"},{"id":2179290363,"name":"Prism 3","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_Color03_thumb.png"},{"id":2189302920,"name":"Team Velocity","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TeamVelocity2019_thumb.png"},{"id":2202078812,"name":"La Bicicletta","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/LaBicicletta_thumb.png"},{"id":2218704109,"name":"Pinarello La Squadra Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/PLS2021_thumb.png"},{"id":2219699088,"name":"Team PTz","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TeamPTZ_thumb.png"},{"id":2242745186,"name":"Endura","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Endura2022_thumb.png"},{"id":2252477074,"name":"Belgium Elite","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BelgiumCyclingFederationElite2020_thumb.png"},{"id":2262974117,"name":"Zwift zFondo March","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_zFondo_Mar_thumb.png"},{"id":2280956876,"name":"INEOS Grenadiers 2022 Training","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/INEOSGrenadiersFan2021_thumb.png"},{"id":2282933836,"name":"Bike & Beer","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BikeNBeer_thumb.png"},{"id":2289295452,"name":"Chasing Cancellara","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ChasingCancellara2020_thumb.png"},{"id":2291562618,"name":"Zwift UCI 2020 Wildcard","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/UCIZwiftWildcard2020_thumb.png"},{"id":2298505566,"name":"Assos 2017","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Assos2017_thumb.png"},{"id":2299049488,"name":"Giant Ride Like King 2018","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/RideLikeKing2018_thumb.png"},{"id":2300165841,"name":"Zwift Women's Day","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_WomensDay_thumb.png"},{"id":2327670378,"name":"Wahoo Climbers","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/WahooClimb2022_thumb.png"},{"id":2330819669,"name":"L39ION of LA 2022","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/L39ION2022_thumb.png"},{"id":2336895499,"name":"Quarq","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Quarq_thumb.png"},{"id":2340825967,"name":"Utsunomiya Blitzen","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/UtsunomiyaBlitzen2019_thumb.png"},{"id":2344969429,"name":"Collins Cup Internationals","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/CollinsCupYellow2020_thumb.png"},{"id":2349035663,"name":"EF Education First","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Cannondale_2014_thumb.png"},{"id":2352794923,"name":"BMC Walmart 2022","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BMCWalmart2022_thumb.png"},{"id":2360518479,"name":"ZTH","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZTH_thumb.png"},{"id":2360890720,"name":"Tour Of Watopia","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TourOfWatopia2020_thumb.png"},{"id":2364275274,"name":"ZA Tri Team Kit 2020","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZATriTeam2021_thumb.png"},{"id":2364410521,"name":"Trek Travel","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TrekTravel2022_thumb.png"},{"id":2376223766,"name":"Cape Epic 2020","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/AbsaCapeEpic2020_thumb.png"},{"id":2386311153,"name":"El Giro de Rigo 2021","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ElGiroDeRigoCR2021_thumb.png"},{"id":2394699876,"name":"GearPatrol","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/GearPatrol2015_thumb.png"},{"id":2397240918,"name":"Cafe du Cycliste","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/CafeDuCycliste2021_thumb.png"},{"id":2398622818,"name":"RoadCC","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/RoadCC_thumb.png"},{"id":2405730976,"name":"Classy 3","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftClassy03_thumb.png"},{"id":2411042224,"name":"Team Poland","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftTeamPL_thumb.png"},{"id":2411973716,"name":"ZFondo February 2022","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZFondo202202_thumb.png"},{"id":2414198484,"name":"INEOS Energy Station","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/INEOSEnergyStation2021_thumb.png"},{"id":2418675542,"name":"JLT Condor","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/JLT_Condor_thumb.png"},{"id":2422819298,"name":"Eolo Kometa","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/EoloKometa2021_thumb.png"},{"id":2426765558,"name":"Black Sheep Man Ride","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BlackSheepManRide2019_thumb.png"},{"id":2431411798,"name":"Trek Factory Racing Women","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TrekFactoryRacingWomensMTB2021_thumb.png"},{"id":2436486671,"name":"Zwift Academy Women","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftAcademyWomen2019_thumb.png"},{"id":2464496177,"name":"Norseman White Jersey","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Norseman2021_thumb.png"},{"id":2465897399,"name":"Z Fondo January","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZFondo2020Jan_thumb.png"},{"id":2470462123,"name":"Camo 1","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_Camo_01_thumb.png"},{"id":2471388684,"name":"ZA Road 2021 Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZARoad2021_thumb.png"},{"id":2474999752,"name":"Team Innovation","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TeamInnovation_thumb.png"},{"id":2476945236,"name":"DBR2021","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/DBR2021_thumb.png"},{"id":2481265759,"name":"Mi Duole","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/MiDuole2021_thumb.png"},{"id":2486273949,"name":"New Zealand Elite","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/NewZealandElite2020_thumb.png"},{"id":2491300846,"name":"Newbury Velo","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/NewburyVelo2020_thumb.png"},{"id":2498899049,"name":"Overall","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_Orange_Jersey_01.png"},{"id":2500137555,"name":"SRAM","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/SRAM_2014_thumb.png"},{"id":2512718712,"name":"Finesse Rockets","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/FinesseRockets2022_thumb.png"},{"id":2515880534,"name":"ZZRC","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZZRC_thumb.png"},{"id":2521862418,"name":"Grand Fondo Team","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/GrandFondoTeam2022_thumb.png"},{"id":2523171976,"name":"Level 25","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_LVL25_Jersey_thumb.png"},{"id":2526576733,"name":"FDJ Nouvelle Aquitaine Futuroscope","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/FDJWomensWTT2022_thumb.png"},{"id":2543336057,"name":"Super League Tri Arena Games - Grey Jersey","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/SLT2022grey_thumb.png"},{"id":2550640930,"name":"Clash Of Clubs Pink","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ClashOfClubs2020_thumb.png"},{"id":2557367984,"name":"dPac-Elite","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/dPaceElite2022_thumb.png"},{"id":2561776795,"name":"KNWU Elite","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/DutchKNWUElite2019_thumb.png"},{"id":2562729010,"name":"MS 150","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/MS150_thumb.png"},{"id":2570714789,"name":"Tacx","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TacxBlack_thumb.png"},{"id":2574836639,"name":"Castelli 2022","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Castelli2022_thumb.png"},{"id":2576426166,"name":"dZi","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TeamdZi_thumb.png"},{"id":2585063482,"name":"Strava","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/StravaBasic2015_thumb.png"},{"id":2592981205,"name":"CCC","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/CCC2019_thumb.png"},{"id":2594010068,"name":"UCI Yorkshire","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/UCIYorkshire2019_thumb.png"},{"id":2599159219,"name":"Basic 5","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Plain_07_thumb.png"},{"id":2607361915,"name":"TugaZ 2021","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TugaZ2021_thumb.png"},{"id":2614650015,"name":"AHDR Liv","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/AHDRLiv_thumb.png"},{"id":2622309203,"name":"Team Vitality Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/VitalityCycling2021_thumb.png"},{"id":2643412332,"name":"Rave","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Rave2022_thumb.png"},{"id":2644587674,"name":"FreeSpeed","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/FreeSpeed2015_thumb.png"},{"id":2651946795,"name":"Trinity","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Trinity2021_thumb.png"},{"id":2668995113,"name":"Vattern Rundan","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/VatternRundan2019_thumb.png"},{"id":2676979249,"name":"Haute Route Ventoux 2020","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/HauteRoutexZwift2020_thumb.png"},{"id":2689503371,"name":"Splunk Race Team","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/SplunkRaceTeam2018_thumb.png"},{"id":2694493012,"name":"TWENTY24 Pro Cycling","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TWENTY242022_thumb.png"},{"id":2695025247,"name":"Team ODZ","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ODZ_thumb.png"},{"id":2697671308,"name":"World Cancer Day","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/WorldCancerDay2021_thumb.png"},{"id":2701885115,"name":"Alinghi","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Alinghi_thumb.png"},{"id":2708360718,"name":"Tour de Tohoku","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TourDeTohoku2018_thumb.png"},{"id":2714003021,"name":"zFondo Dec 2020","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZFondo2020Dec_thumb.png"},{"id":2721512078,"name":"Portugal Elite","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/PortugalElite2020_thumb.png"},{"id":2727023190,"name":"Sprint/Overall","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_Green_Orange.png"},{"id":2736225678,"name":"ZHCC","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZHCC2019_thumb.png"},{"id":2738115720,"name":"Bahati Foundation","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BahatiFoundation2018_thumb.png"},{"id":2738257269,"name":"NGNM WRRS 2021 Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/WHM2021_thumb.png"},{"id":2750221242,"name":"Tour de France Green","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TdFGreen2020_thumb.png"},{"id":2750224195,"name":"Gore","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Gore2019_thumb.png"},{"id":2751540424,"name":"Canberra CC","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/CanberraClub_thumb.png"},{"id":2751735592,"name":"Strava Premium","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/StravaPremium_2015_thumb.png"},{"id":2752872281,"name":"Team WBR","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TeamWBR_thumb.png"},{"id":2754191702,"name":"Castelli Virtual Aero Dark","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/CastelliVirtualClimber_Dark_thumb.png"},{"id":2757515117,"name":"Trek","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Trek_2014_thumb.png"},{"id":2758001656,"name":"Black Sheep Black","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BlackSheepBlack2019_thumb.png"},{"id":2763945545,"name":"Switzerland Elite","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/SwissElite2020_thumb.png"},{"id":2765663061,"name":"Poland Elite","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/PolandElite2020_thumb.png"},{"id":2767775528,"name":"Trinity Racing","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TrinityRacing2020_thumb.png"},{"id":2768366528,"name":"Team ODZ 2018","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ODZ2018Mens_thumb.png"},{"id":2784272641,"name":"Garneau Club 2018","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/GarneauClub2018_thumb.png"},{"id":2793486014,"name":"Bikeradar","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Bikeradar_thumb.png"},{"id":2795352821,"name":"Team EF Education-TIBCO-SVB","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TeamEFEd2022_thumb.png"},{"id":2795876350,"name":"Cervelo","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Cervelo_thumb.png"},{"id":2799052383,"name":"ZRG Cycling Club 2021","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftRidersGermany2018_thumb.png"},{"id":2802328031,"name":"Outside Magazine","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/OutsideMagazine2021_thumb.png"},{"id":2810981379,"name":"Champion","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Plain_06_thumb.png"},{"id":2813571014,"name":"Team BikeExchange Men","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BikeExchangeMen2022_thumb.png"},{"id":2818028945,"name":"DigiCamo 1","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_CamoPix_01_thumb.png"},{"id":2825077965,"name":"La Z Claire","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_80s_thumb.png"},{"id":2826950973,"name":"Monochrome 1","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_Monochrome01_thumb.png"},{"id":2855133168,"name":"Trek Training Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Trek_2017_Yellow_thumb.png"},{"id":2856076954,"name":"2021 Virtual Series Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/OVSPlain2021_thumb.png"},{"id":2872877007,"name":"Mike's Bikes","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/MikesBikes2022_thumb.png"},{"id":2879713688,"name":"Downing Cycling Donny Chaingang","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Downing2021_thumb.png"},{"id":2885953440,"name":"RO4H","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/RO4H2021_thumb.png"},{"id":2888849980,"name":"MIRT Community Club","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/MIRT2021_thumb.png"},{"id":2889081010,"name":"Level 50","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_LVL50_thumb.png"},{"id":2891579327,"name":"Zwift Academy Women 2018","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftAcademyWomen2018_thumb.png"},{"id":2906189156,"name":"Deceuninck-Quick-Step","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Quickstep2020_thumb.png"},{"id":2911653862,"name":"Trek Factory Racing Womens MTB","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TrekFactoryRacingWomensMTB2020_thumb.png"},{"id":2918338388,"name":"Vox Women 2019","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/VoxWomen2019_thumb.png"},{"id":2929288885,"name":"Zwift Academy Tri 2019","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftAcademyTri2020_thumb.png"},{"id":2933220037,"name":"ODZ Men","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ODZMen2021_thumb.png"},{"id":2940035943,"name":"Gold Coast","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/GoldCoast_thumb.png"},{"id":2945585694,"name":"Power Up","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/PowerUp2019_thumb.png"},{"id":2953531890,"name":"Screen Machine 2018","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ScreenMachine2018_thumb.png"},{"id":2966777958,"name":"Aero 2020","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Aero2020_thumb.png"},{"id":2976426174,"name":"Tour De Yorkshire","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TourDeYorkshire2018_thumb.png"},{"id":2979338200,"name":"Van Rysel Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/VanRysel2021_thumb.png"},{"id":2984660529,"name":"Tour de France White","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TdFWhite2020_thumb.png"},{"id":2986648336,"name":"Classy 2","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftClassy02_thumb.png"},{"id":2993458510,"name":"Canyon Eisberg","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/CanyonEisberg_thumb.png"},{"id":2995674682,"name":"Zwift zFondo February 2019","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftZFondoFeb2019_thumb.png"},{"id":2997085668,"name":"ZFondo March 2022","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZFondo202203_thumb.png"},{"id":2998551931,"name":"Angola Federation Elite 2022","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/UCIBlackAngola2022_thumb.png"},{"id":3002321123,"name":"Giant","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Giant2017_thumb.png"},{"id":3016677240,"name":"Zwift zFondo August","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_zFondo2018_Aug_thumb.png"},{"id":3020536025,"name":"Groupama FDJ","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/FDJMens2020_thumb.png"},{"id":3027591243,"name":"WKG","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/WKGWattsUp_thumb.png"},{"id":3053089758,"name":"Super League Tri Arena Games - Red Jersey","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/SLT2022red_thumb.png"},{"id":3058092866,"name":"Castelli Virtual Aero Red","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/CastelliVirtualAero_Red_thumb.png"},{"id":3061523465,"name":"Rowe & King","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/RoweAndKing_thumb.png"},{"id":3068939527,"name":"Cycling Tips","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/CyclingTips_thumb.png"},{"id":3073658735,"name":"Haute Route Watopia 2021 Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/HauteRoute2021_thumb.png"},{"id":3084206699,"name":"Rad Race","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/RadRace_thumb.png"},{"id":3090729076,"name":"Z Racing A","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZRacingA_thumb.png"},{"id":3097301453,"name":"Ride With Reason","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/RideWithReason2018_thumb.png"},{"id":3103938066,"name":"Lotto-Soudal","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/LottoSoudal2017_thumb.png"},{"id":3105493392,"name":"Garmin","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Garmin_thumb.png"},{"id":3107176911,"name":"MAAP2020","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/MAAP2020_thumb.png"},{"id":3107867912,"name":"Black Sheep White","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BlackSheepWhite2019_thumb.png"},{"id":3127040018,"name":"CCP Zwift Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/CCP_Zwift_Kit_thumb.png"},{"id":3148463699,"name":"Alpecin-Fenix 2022","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/AlpecinFenix2022_thumb.png"},{"id":3150462918,"name":"2020 Esports World Champ M","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/CyclingEsportsWC2021_thumb.png"},{"id":3157550744,"name":"World Bicycle Relief 2018","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/WBRKit2018_thumb.png"},{"id":3162864971,"name":"Prism 2","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_Color02_thumb.png"},{"id":3163204428,"name":"NEXT","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Next20201_thumb.png"},{"id":3168395763,"name":"Zwift zFondo June","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_zFondo2018_Jun_thumb.png"},{"id":3169737262,"name":"Team Veselka","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TeamVeselka2022_thumb.png"},{"id":3173711529,"name":"Team Type 1","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TeamType1_thumb.png"},{"id":3185255681,"name":"BCBR Gravel Explorer Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BCBRGravel2021_thumb.png"},{"id":3189709644,"name":"Biehler Syndicate Jersey","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BiehlerSyndicate2021_thumb.png"},{"id":3203180937,"name":"ZNC - Great Britain","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftNationalChampions_thumb.png"},{"id":3206315444,"name":"British Triathlon Member","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BritishTriathlonMember2018_thumb.png"},{"id":3206625974,"name":"Australia Elite","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/AustraliaElite2020_thumb.png"},{"id":3208371570,"name":"GTN","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/GTN2019_thumb.png"},{"id":3219469326,"name":"KZR navy","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/KZRnavy_thumb.png"},{"id":3235061071,"name":"WEZ 2020","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/WEZ2020_thumb.png"},{"id":3236010357,"name":"Women's Ride Series 2022","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftWomens2022_thumb.png"},{"id":3237599752,"name":"Zwift zFondo December 2018","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftZFondoDec2018_thumb.png"},{"id":3241984882,"name":"Team ZF","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftFitness2018_thumb.png"},{"id":3246030373,"name":"KISS 2017","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/KISS_2017_thumb.png"},{"id":3246186957,"name":"Tour de France KOM","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TdFKOM2020_thumb.png"},{"id":3252508495,"name":"Zwift eFondo","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_eFondo_thumb.png"},{"id":3258262470,"name":"Trek Factory Racing Mens MTB","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TrekFactoryRacingMensMTB2020_thumb.png"},{"id":3264337015,"name":"Scott Sram","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ScottSram2020_thumb.png"},{"id":3271072532,"name":"Z Racing C","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZRacingC_thumb.png"},{"id":3275729930,"name":"100km","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_MetricCentury_Jersey_thumb.png"},{"id":3286294090,"name":"Zwift Academy","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftAcademy_thumb.png"},{"id":3297463904,"name":"Japan Elite","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/JapanElite2020_thumb.png"},{"id":3305515323,"name":"South Africa Elite","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/SouthAfricaElite2020_thumb.png"},{"id":3310223345,"name":"QOM","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/QOM.png"},{"id":3316214471,"name":"Battenkill","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Battenkill_2015_thumb.png"},{"id":3318118077,"name":"Italy Federation Elite 2022","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ItalyCyclingFederationElite2020_thumb.png"},{"id":3323572883,"name":"Zwift zFondo July","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_zFondo2018_Jul_thumb.png"},{"id":3324207353,"name":"Pioneer","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Pioneer_thumb.png"},{"id":3334670194,"name":"Giro De Rigo","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/GiroDeRigo2020_thumb.png"},{"id":3348192842,"name":"zFondo Feb 2021","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZFondo2021Feb_thumb.png"},{"id":3351653575,"name":"dZi Red","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TeamdZi_Red_thumb.png"},{"id":3352564281,"name":"Canyon Esports 2021","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/CanyonEsports2021_thumb.png"},{"id":3352739305,"name":"Souigneur","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Souigneur_thumb.png"},{"id":3361977988,"name":"ZFondo January 2022","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZFondo202201_thumb.png"},{"id":3370254609,"name":"Trek Tour","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TrekTour2019_thumb.png"},{"id":3371519326,"name":"Overall/KOM","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_Orange_KOM.png"},{"id":3372672430,"name":"Valcar-Travel and Service","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ValcarCylance2019_thumb.png"},{"id":3375581593,"name":"Team Novo Nordisk","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/NovoNordisk_thumb.png"},{"id":3382526362,"name":"USAC Elite","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/USAC_thumb.png"},{"id":3384238636,"name":"Androni Giocattoli-Sidermec","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/AndroniGiocattoli2019_thumb.png"},{"id":3385788360,"name":"XTERRA TRI","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Xterra2020_thumb.png"},{"id":3388151259,"name":"Zwift Insider","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftInsider2022_thumb.png"},{"id":3405367476,"name":"Evoq.Bike","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/EvoqBike2020_thumb.png"},{"id":3405631997,"name":"ATOC KOM","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ATOC_KOM_2017.png"},{"id":3407909128,"name":"World Bicycle Relief","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/WBRKit2019_thumb.png"},{"id":3419241775,"name":"Agowatt","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Agowatt2021_thumb.png"},{"id":3419530421,"name":"ODZ Women","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ODZWomen2021_thumb.png"},{"id":3421325459,"name":"Rapha W100","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Rapha100km2018_thumb.png"},{"id":3423381874,"name":"Absque Fines","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/AbsqueFines2021_thumb.png"},{"id":3424931689,"name":"Canada Elite","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/CyclingCanada2020_thumb.png"},{"id":3427039185,"name":"April Fools","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TealPink2020_thumb.png"},{"id":3427652207,"name":"Liv Ride Like King 2018","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/RideLikeKing2018_thumb.png"},{"id":3439799665,"name":"Jan Frodeno","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/JanFrodeno2019_thumb.png"},{"id":3440237765,"name":"Team Cryo","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TeamCryo2019_thumb.png"},{"id":3446412723,"name":"Enshored","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Enshored2021_thumb.png"},{"id":3450131175,"name":"Specialized M","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Specialized_thumb.png"},{"id":3451819302,"name":"Rapha Women's Souplesse","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/RaphaSouplesse02_thumb.png"},{"id":3455961079,"name":"Plush Global Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/PlushGlobal2021_thumb.png"},{"id":3456451761,"name":"Team Racing Without Borders Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TRWB2022_thumb.png"},{"id":3459652898,"name":"Schleck Gran Fondo","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/SchleckGranFondo2018_thumb.png"},{"id":3459897545,"name":"2021 Pride On Cycling Jersey","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/PrideOn2021_thumb.png"},{"id":3465825532,"name":"Restart Racing 2021","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/RestartRacing2021_thumb.png"},{"id":3470268490,"name":"Saris","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Saris2019_thumb.png"},{"id":3471138888,"name":"ASTANA WOMEN PRO TEAM","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/AstanaWomens_thumb.png"},{"id":3488472716,"name":"Today's Plan","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TodaysPlan_thumb.png"},{"id":3503002798,"name":"Bardiani 2019","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Bardiani2019_thumb.png"},{"id":3526847660,"name":"Ryzon 2021","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Ryzon2021_thumb.png"},{"id":3535768157,"name":"Monochrome 3","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_Monochrome03_thumb.png"},{"id":3535883671,"name":"Rye","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Rye2015_thumb.png"},{"id":3540949175,"name":"Roxsolt Liv SRAM","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/RoxsaltAttaquer2021_thumb.png"},{"id":3542291835,"name":"Zwift zFondo November 2018","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftZFondoNov2018_thumb.png"},{"id":3553917933,"name":"Wahoo","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Wahoo_2014_thumb.png"},{"id":3559497553,"name":"Endure IQ Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/EndureIQ2021_thumb.png"},{"id":3559927551,"name":"Hot Tubes","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/HotTubes2019_thumb.png"},{"id":3571464315,"name":"Camo 2","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_Camo_02_thumb.png"},{"id":3571672586,"name":"Rapha Flyweight","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/RaphaClassicFlyweight2019_thumb.png"},{"id":3574886872,"name":"World Bicycle Relief 2016","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/WorldBicycleRelief2016_thumb.png"},{"id":3580594002,"name":"Zwift zFondo December","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_zFondo_Dec_thumb.png"},{"id":3581804427,"name":"Pushing Limits 2021 Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/PushingLimits2021_thumb.png"},{"id":3591639561,"name":"GSR Long Riders","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/GSRLongRiders2016_thumb.png"},{"id":3591907082,"name":"Toyota CRYO RTD Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ToyotaCryoRDT2021_thumb.png"},{"id":3593181577,"name":"GMBN","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/GMBN2020_thumb.png"},{"id":3602307922,"name":"SISU Racing Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/SISURacing2022_thumb.png"},{"id":3606536172,"name":"Rapha Cycling Club","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/RCC2016_thumb.png"},{"id":3612223524,"name":"NTT Pro Cycling Team","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/NTT2020_thumb.png"},{"id":3613277480,"name":"Sydney CC","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/SydneyCC2015_thumb.png"},{"id":3613279582,"name":"Rapha Festive 500","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Rapha5002020_thumb.png"},{"id":3613318982,"name":"Beat Club 2020","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BeatClub2020_thumb.png"},{"id":3615789764,"name":"TRUE","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/True2020_thumb.png"},{"id":3630089540,"name":"Rouleur 2020","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Rouleur2020_thumb.png"},{"id":3631135127,"name":"KISS","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/KISS_thumb.png"},{"id":3640900519,"name":"Movember","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Movember2019_thumb.png"},{"id":3652093952,"name":"I Race Like A Girl","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/IRaceLikeAGirl2019_thumb.png"},{"id":3652158933,"name":"Team Turbo","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Turbo2019_thumb.png"},{"id":3654809610,"name":"Supersonic Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/FunIsFast2021_thumb.png"},{"id":3657231940,"name":"PAS Racing","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/PASRacing2022_thumb.png"},{"id":3660609812,"name":"World Social Riders","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/WSR_thumb.png"},{"id":3660671907,"name":"Zwift Academy 2020 Mens","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftAcademyMen2020_thumb.png"},{"id":3674985275,"name":"KZR white","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/KZRwhite_thumb.png"},{"id":3687043191,"name":"Betty Designs Signature","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BettyDesignsSignature_thumb.png"},{"id":3694781490,"name":"Internationelles","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Internationelles2021_thumb.png"},{"id":3699128371,"name":"Scott Sram Champion","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ScottSram2020Champ_thumb.png"},{"id":3706069690,"name":"Strava Subscriber","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/StravaPremium2021_thumb.png"},{"id":3710487136,"name":"Maratona Fondo","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/MaratonaFondo2018_thumb.png"},{"id":3711428337,"name":"DigiCamo 3","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_CamoPix_03_thumb.png"},{"id":3712778083,"name":"Basic 3","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Plain_04_thumb.png"},{"id":3712887856,"name":"Hot Chilee","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/HotChilee2019_thumb.png"},{"id":3715802624,"name":"dPAC","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Dpac2020_thumb.png"},{"id":3720531761,"name":"SigmaSport","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/SigmaSport_thumb.png"},{"id":3727543886,"name":"Canyon","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Canyon_thumb.png"},{"id":3743745974,"name":"Eliel Fruit Fondo","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/VeganCyclists2022_thumb.png"},{"id":3750784950,"name":"Bolt Race Team","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BRTMens2019_thumb.png"},{"id":3752612147,"name":"JETT","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/JETT2021_thumb.png"},{"id":3757074019,"name":"CIS Training Systems","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/CIS_thumb.png"},{"id":3761002195,"name":"Basic 4","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Plain_05_thumb.png"},{"id":3763851073,"name":"DigiCamo 2","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_CamoPix_02_thumb.png"},{"id":3773181359,"name":"Tour De Zwift","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TourDeZwift2018_thumb.png"},{"id":3775109784,"name":"Rapha Women's Classic","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/RaphaClassic02_thumb.png"},{"id":3783244115,"name":"ZNC - Sweden","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftNationalChampions_thumb.png"},{"id":3784612177,"name":"Germany Elite","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/GermanyElite2020_thumb.png"},{"id":3793210489,"name":"Sprint","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_Green_Jersey_01.png"},{"id":3793940065,"name":"Sub 7","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Sub72022_thumb.png"},{"id":3795276190,"name":"Team DIRT","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TeamDIRT2019_thumb.png"},{"id":3808857926,"name":"WHOOP Cycling Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Whoop2021_thumb.png"},{"id":3812986196,"name":"Triathlete","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Triathlete_thumb.png"},{"id":3816014088,"name":"Supersapiens","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Supersapiens2021_thumb.png"},{"id":3819355988,"name":"Socks 4 Watts 2021","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Socks4Watts2021_thumb.png"},{"id":3824102442,"name":"Danish Cycling Elite","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/DanishCyclingElite2019_thumb.png"},{"id":3824368360,"name":"Ride Like King 13","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/RideLikeAKing2021_thumb.png"},{"id":3839989188,"name":"NSI","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/NSI2015_thumb.png"},{"id":3858299788,"name":"ZSUN","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZSUN_thumb.png"},{"id":3858579217,"name":"Team ODivaZ 2018","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ODZ2018Womens_thumb.png"},{"id":3859182028,"name":"The Carnival 2020","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TheCarnival2020_thumb.png"},{"id":3864361640,"name":"Zwift zFondo January","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_zFondo_Jan_thumb.png"},{"id":3871254116,"name":"Lionel Sanders","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/LionelSanders2019_thumb.png"},{"id":3875378752,"name":"Black Sheep Salmon","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BlackSheepSalmon2019_thumb.png"},{"id":3875660873,"name":"Voxwomen Tour","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/VoxWoman2021_thumb.png"},{"id":3876986530,"name":"Dutch Diesel Cycling","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/DutchDieselCycling_thumb.png"},{"id":3877890169,"name":"Challenged Athletes Foundation","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/CAF2022_thumb.png"},{"id":3879389572,"name":"TWENTY24","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Twenty242021_thumb.png"},{"id":3880181922,"name":"Anna van der Breggen","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/AnnaVDB2021_thumb.png"},{"id":3881428952,"name":"ZNC - Austria","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftNationalChampions_thumb.png"},{"id":3882206422,"name":"Heino","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Heino2020_thumb.png"},{"id":3891106714,"name":"Toronto Hustle Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TorontoHustle2021_thumb.png"},{"id":3891872995,"name":"Beastmode","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BeastMode2021_thumb.png"},{"id":3895671417,"name":"Qhubeka Assos Pro Team Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/QhubekaAssos2021_thumb.png"},{"id":3900171318,"name":"ZNC - USA","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftNationalChampions_thumb.png"},{"id":3901439314,"name":"Mandela Day 2021","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/MandelaDay2021_thumb.png"},{"id":3902243512,"name":"Bikes France","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BikesFrance2022_thumb.png"},{"id":3909717131,"name":"Richmond 2015","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Richmond_2015_thumb.png"},{"id":3917484491,"name":"Camo 3","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_Camo_03_thumb.png"},{"id":3926835792,"name":"Kinetic","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Kinetic_thumb.png"},{"id":3928901190,"name":"Rapha Festive 500","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/RaphaFestive5002021_thumb.png"},{"id":3932519699,"name":"Liv Racing 2019","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/LivRacing2019_thumb.png"},{"id":3940719237,"name":"Zwift UCI 2020 Champion","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/UCIZwiftChamps2020_thumb.png"},{"id":3955133527,"name":"Super League Tri Arena Games - Yellow Jersey","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/SLT2022yellow_thumb.png"},{"id":3957077256,"name":"ZNC - France","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftNationalChampions_thumb.png"},{"id":3970245639,"name":"CANYON//SRAM Racing","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/CanyonSRAMRacing2022_thumb.png"},{"id":3977378563,"name":"Clash Of Clubs Green","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ClashOfClubs2020_thumb.png"},{"id":3988640695,"name":"Rapha L'Etape","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/RaphaLetape2019_thumb.png"},{"id":3988726887,"name":"CeramicSpeed","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/CeramicSpeed2020_thumb.png"},{"id":3992094603,"name":"Super League Tri Arena Games - Black Jersey","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/SLT2022black_thumb.png"},{"id":3992154832,"name":"Garmin Unbound","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/GarminUnbound2021_thumb.png"},{"id":3997512675,"name":"Austria Elite","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/AustriaElite2020_thumb.png"},{"id":3998221209,"name":"Team Ascenders","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TeamAscenders_thumb.png"},{"id":4001744157,"name":"Zwift Academy 2017","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftAcademyMen2017_thumb.png"},{"id":4002448899,"name":"Super League Tri Arena Games - Purple Jersey","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/SLT2022purple_thumb.png"},{"id":4002790487,"name":"Epic QOM","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/QOM_Epic.png"},{"id":4021434624,"name":"ATOC Sprint","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ATOC_Sprint_2017.png"},{"id":4024285339,"name":"Women of Colour Cycling Collective","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/WCCC2022_thumb.png"},{"id":4024411117,"name":"Monochrome 2","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_Monochrome02_thumb.png"},{"id":4029066174,"name":"LEquipe Provence 2021","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/LEquipeProvence2021_thumb.png"},{"id":4037333831,"name":"KNWU Member","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/DutchKNWUMember2019_thumb.png"},{"id":4040512860,"name":"RadRace 2021","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/RadRace2021_thumb.png"},{"id":4044089034,"name":"R3R 2020","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/R3R2020_thumb.png"},{"id":4044385147,"name":"BZR Sportsolid","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BZRSportsolid2021_thumb.png"},{"id":4046322655,"name":"Liv Ride Like King","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/RideLikeKing2019_thumb.png"},{"id":4046534177,"name":"Vitus","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Vitus2019_thumb.png"},{"id":4051240783,"name":"Cycling Hub","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/CyclingHub2018_thumb.png"},{"id":4053742767,"name":"Human Powered Health Pro","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/HPHP2022_thumb.png"},{"id":4055466522,"name":"Team Kalas","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TeamKalas2020_thumb.png"},{"id":4065189818,"name":"Z Fondo March","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZFondo2020Mar_thumb.png"},{"id":4070625544,"name":"Rapha Rising 2022","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/RaphaRising2022_thumb.png"},{"id":4071133758,"name":"Tour of Watopia 2022","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TOW2022_thumb.png"},{"id":4072760162,"name":"USA Triathlon","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/USATri2022_thumb.png"},{"id":4074781084,"name":"Optum","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Optum2015_thumb.png"},{"id":4076007077,"name":"PO AUTO","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/POAuto2020_thumb.png"},{"id":4081455159,"name":"Super League Tri Arena Games - Orange Jersey","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/SLT2022orange_thumb.png"},{"id":4082758009,"name":"Pride On","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/PrideOn2020_thumb.png"},{"id":4090819303,"name":"Basecamp Kit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Basecamp2021_thumb.png"},{"id":4095063053,"name":"World Bicycle Relief 2015","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/WorldBikeRelief2015_thumb.png"},{"id":4100199100,"name":"ZNC - Australia","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftNationalChampions_thumb.png"},{"id":4102459937,"name":"Parkhotel Valkenburg","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ParkHotelValkenburg2020_thumb.png"},{"id":4107861598,"name":"Super League Tri Arena Games - Pink Jersey","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/SLT2022pink_thumb.png"},{"id":4108702181,"name":"Rapha Legion LA","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/RaphaLegionLA2020_thumb.png"},{"id":4109295337,"name":"Dutch National","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/DutchNational_thumb.png"},{"id":4113250696,"name":"Team Zoot 2022","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TeamZoot2022_thumb.png"},{"id":4115647532,"name":"Hagens Berman Supermint","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/HagensBermanSupermint2019_thumb.png"},{"id":4121221568,"name":"Classy 1","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftClassy01_thumb.png"},{"id":4123532946,"name":"KOA Sports","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/KoaSports2019_thumb.png"},{"id":4130579852,"name":"Lotto","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Lotto_2015_thumb.png"},{"id":4137478375,"name":"KOM","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/KOM_01.png"},{"id":4143072100,"name":"Zwift zFondo April 2019","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftZFondoApr2019_thumb.png"},{"id":4144972561,"name":"Northhampton CC","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/NorthhamptonCC_thumb.png"},{"id":4159702566,"name":"Tour de France Yellow","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TdFYellow2020_thumb.png"},{"id":4169441149,"name":"Zwift NL Community","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftNL2021_thumb.png"},{"id":4186643812,"name":"BZR","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BZR2019_thumb.png"},{"id":4189588969,"name":"Aeonian 2021","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Aeonian2021_thumb.png"},{"id":4190704551,"name":"London Dynamo","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/LondonDynamo_thumb.png"},{"id":4191972189,"name":"Cofidis","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Cofidis2019_thumb.png"},{"id":4199295486,"name":"ZNC - Denmark","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftNationalChampions_thumb.png"},{"id":4209581234,"name":"Aurum","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Aurum2021_thumb.png"},{"id":4213507995,"name":"Prism 1","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_Color01_thumb.png"},{"id":4213990690,"name":"Athlonia","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Athlonia_thumb.png"},{"id":4225469098,"name":"USAC Member","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/USACMember2020_thumb.png"},{"id":4235392112,"name":"USMES","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/USMES2016_thumb.png"},{"id":4237906310,"name":"Team DiData Fan","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TeamDiDataFan2018_thumb.png"},{"id":4248910215,"name":"Revolution Velo Racing","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ReVoKit2018_thumb.png"},{"id":4252106063,"name":"Team Fusion","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TeamFusion2019_thumb.png"},{"id":4256936040,"name":"Clean Sweep","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_Green_Orange_KOM.png"},{"id":4258677330,"name":"KZR red","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/KZRred_thumb.png"},{"id":4260352271,"name":"ZNC - Japan","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZwiftNationalChampions_thumb.png"},{"id":4266694480,"name":"Team TFC","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TeamTFC2018_thumb.png"},{"id":4283763318,"name":"Trek Factory Racing Men","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TrekFactoryRacingMensMTB2021_thumb.png"},{"id":4288197284,"name":"Z Racing B","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZRacingB_thumb.png"}],"notableMomentTypes":[{"id":0,"name":"NEW PR!","listImageUrl":"https://cdn.zwift.com/static/zc/NOTABLE_MOMENT_TYPES/list/NEW_PR.png","mapImageUrl":"https://cdn.zwift.com/static/zc/NOTABLE_MOMENT_TYPES/map/NEW_PR.png","priority":1},{"id":1,"name":"HIT DAILY TARGET!","listImageUrl":"https://cdn.zwift.com/static/zc/NOTABLE_MOMENT_TYPES/list/MET_DAILY_TARGET.png","mapImageUrl":"https://cdn.zwift.com/static/zc/NOTABLE_MOMENT_TYPES/map/MET_DAILY_TARGET.png","priority":8},{"id":2,"name":"ACHIEVEMENT UNLOCKED!","listImageUrl":"https://cdn.zwift.com/static/zc/NOTABLE_MOMENT_TYPES/list/ACHIEVEMENT_UNLOCKED.png","mapImageUrl":"https://cdn.zwift.com/static/zc/NOTABLE_MOMENT_TYPES/map/ACHIEVEMENT_UNLOCKED.png","priority":5},{"id":3,"name":"MISSION COMPLETED","listImageUrl":"https://cdn.zwift.com/static/zc/NOTABLE_MOMENT_TYPES/list/MISSION_COMPLETED.png","mapImageUrl":"https://cdn.zwift.com/static/zc/NOTABLE_MOMENT_TYPES/map/MISSION_COMPLETED.png","priority":6},{"id":4,"name":"UNLOCKED ITEM","listImageUrl":"https://cdn.zwift.com/static/zc/NOTABLE_MOMENT_TYPES/list/UNLOCKED_ITEM.png","mapImageUrl":"https://cdn.zwift.com/static/zc/NOTABLE_MOMENT_TYPES/map/UNLOCKED_ITEM.png","priority":4},{"id":5,"name":"LEVEL UP!","listImageUrl":"https://cdn.zwift.com/static/zc/NOTABLE_MOMENT_TYPES/list/GAINED_LEVEL.png","mapImageUrl":"https://cdn.zwift.com/static/zc/NOTABLE_MOMENT_TYPES/map/GAINED_LEVEL.png","priority":2},{"id":8,"name":"TOOK A JERSEY","listImageUrl":"https://cdn.zwift.com/static/zc/NOTABLE_MOMENT_TYPES/list/TOOK_ARCH_JERSEY.png","mapImageUrl":"https://cdn.zwift.com/static/zc/NOTABLE_MOMENT_TYPES/map/TOOK_ARCH_JERSEY.png","priority":9},{"id":10,"name":"COMPLETED A GOAL","listImageUrl":"https://cdn.zwift.com/static/zc/NOTABLE_MOMENT_TYPES/list/COMPLETED_GOAL.png","mapImageUrl":"https://cdn.zwift.com/static/zc/NOTABLE_MOMENT_TYPES/map/COMPLETED_GOAL.png","priority":7},{"id":13,"name":"FINISHED EVENT","listImageUrl":"https://cdn.zwift.com/static/zc/NOTABLE_MOMENT_TYPES/list/FINISHED_EVENT.png","mapImageUrl":"https://cdn.zwift.com/static/zc/NOTABLE_MOMENT_TYPES/map/FINISHED_EVENT.png","priority":11},{"id":15,"name":"FINISHED WORKOUT","listImageUrl":"https://cdn.zwift.com/static/zc/NOTABLE_MOMENT_TYPES/list/FINISHED_WORKOUT.png","mapImageUrl":"https://cdn.zwift.com/static/zc/NOTABLE_MOMENT_TYPES/map/FINISHED_WORKOUT.png","priority":12},{"id":17,"name":"FINISHED CHALLENGE","listImageUrl":"https://cdn.zwift.com/static/zc/NOTABLE_MOMENT_TYPES/list/FINISHED_CHALLENGE.png","mapImageUrl":"https://cdn.zwift.com/static/zc/NOTABLE_MOMENT_TYPES/map/FINISHED_CHALLENGE.png","priority":10},{"id":19,"name":"TRAINING PLAN COMPLETE!","listImageUrl":"https://cdn.zwift.com/static/zc/NOTABLE_MOMENT_TYPES/list/TRAINING_PLAN_COMPLETE.png","mapImageUrl":"https://cdn.zwift.com/static/zc/NOTABLE_MOMENT_TYPES/map/TRAINING_PLAN_COMPLETE.png","priority":3},{"id":20,"name":"ACTIVITY BESTS","listImageUrl":"https://cdn.zwift.com/static/zc/NOTABLE_MOMENT_TYPES/list/ACTIVITY_BESTS.png","mapImageUrl":"https://cdn.zwift.com/static/zc/NOTABLE_MOMENT_TYPES/map/ACTIVITY_BESTS.png","priority":13}],"trainingPlans":[{"id":23939648,"name":"Zwift Employee: High-Mileage Half","imageUrl":"https://cdn.zwift.com/static/zc/TRAINING_PLANS/Wordmark_ZwiftInternal.png"},{"id":142075445,"name":"CRIT Crusher","imageUrl":"https://cdn.zwift.com/static/zc/TRAINING_PLANS/Wordmark_CritCrusher.png"},{"id":160527161,"name":"Zwift Racing","imageUrl":"https://cdn.zwift.com/static/zc/TRAINING_PLANS/Wordmark_ZwiftRacing.png"},{"id":359714946,"name":"Dirt Destroyer","imageUrl":"https://cdn.zwift.com/static/zc/TRAINING_PLANS/Wordmark_DirtDestroyer.png"},{"id":622710059,"name":"ZWIFT 101: Running","imageUrl":"https://cdn.zwift.com/static/zc/TRAINING_PLANS/Wordmark_101run.png"},{"id":635512089,"name":"Fondo","imageUrl":"https://cdn.zwift.com/static/zc/TRAINING_PLANS/Wordmark_Fondo.png"},{"id":884034614,"name":"Zwift Special Access: Coast Ride","imageUrl":"https://cdn.zwift.com/static/zc/TRAINING_PLANS/Wordmark_ZwiftInternal.png"},{"id":945347367,"name":"Watopia Half-Marathon","imageUrl":"https://cdn.zwift.com/static/zc/TRAINING_PLANS/Wordmark_watopiahalfmarathon.png"},{"id":972360468,"name":"Gran Fondo","imageUrl":"https://cdn.zwift.com/static/zc/TRAINING_PLANS/Wordmark_GranFondo.png"},{"id":1064629577,"name":"Multisport Mixer","imageUrl":"https://cdn.zwift.com/static/zc/TRAINING_PLANS/Wordmark_MultiSportMixer.png"},{"id":1160472062,"name":"Build Me Up","imageUrl":"https://cdn.zwift.com/static/zc/TRAINING_PLANS/Wordmark_BuildMeUp.png"},{"id":1200543035,"name":"3 Run 13.1","imageUrl":"https://cdn.zwift.com/static/zc/TRAINING_PLANS/Wordmark_3x13.1.png"},{"id":1499915840,"name":"ZWIFT 101: CYCLING","imageUrl":"https://cdn.zwift.com/static/zc/TRAINING_PLANS/Wordmark_101cycle.png"},{"id":1803127634,"name":"FTP Builder","imageUrl":"https://cdn.zwift.com/static/zc/TRAINING_PLANS/Wordmark_FTPBuilder.png"},{"id":1933759722,"name":"Back to Fitness","imageUrl":"https://cdn.zwift.com/static/zc/TRAINING_PLANS/Wordmark_BacktoFitness_Cycling.png"},{"id":1979360334,"name":"PRL46 2019","imageUrl":"https://cdn.zwift.com/static/zc/TRAINING_PLANS/Wordmark_PRL46.png"},{"id":2048408838,"name":"Active Offseason","imageUrl":"https://cdn.zwift.com/static/zc/TRAINING_PLANS/Wordmark_Offseason.png"},{"id":2318701312,"name":"5k Record Breaker Lite","imageUrl":"https://cdn.zwift.com/static/zc/TRAINING_PLANS/Wordmark_RecordBreaker.png"},{"id":2415283082,"name":"PRL100 2019","imageUrl":"https://cdn.zwift.com/static/zc/TRAINING_PLANS/Wordmark_PRL100.png"},{"id":2904124098,"name":"Cyclist to 10k","imageUrl":"https://cdn.zwift.com/static/zc/TRAINING_PLANS/Wordmark_BikeTo10k.png"},{"id":2959976603,"name":"ZWIFT 201: Your First 5K","imageUrl":"https://cdn.zwift.com/static/zc/TRAINING_PLANS/Wordmark_201run.png"},{"id":3117040564,"name":"Los Angeles Marathon: Phase 2","imageUrl":"https://cdn.zwift.com/static/zc/TRAINING_PLANS/Wordmark_LAmarathon.png"},{"id":3320474959,"name":"Gravel Grinder","imageUrl":"https://cdn.zwift.com/static/zc/TRAINING_PLANS/Wordmark_GravelGrinder.png"},{"id":3653621764,"name":"3 Run 13.1 Lite","imageUrl":"https://cdn.zwift.com/static/zc/TRAINING_PLANS/Wordmark_3x13.1.png"},{"id":3973582598,"name":"Pebble Pounder","imageUrl":"https://cdn.zwift.com/static/zc/TRAINING_PLANS/Wordmark_PebblePounder.png"},{"id":4034427165,"name":"5k Record Breaker","imageUrl":"https://cdn.zwift.com/static/zc/TRAINING_PLANS/Wordmark_RecordBreaker.png"},{"id":4207599990,"name":"TT Tune-Up","imageUrl":"https://cdn.zwift.com/static/zc/TRAINING_PLANS/Wordmark_TTTuneUp.png"},{"id":4268374372,"name":"Los Angeles Marathon: Phase 1","imageUrl":"https://cdn.zwift.com/static/zc/TRAINING_PLANS/Wordmark_LAmarathon.png"},{"id":4274214342,"name":"Singletrack Slayer","imageUrl":"https://cdn.zwift.com/static/zc/TRAINING_PLANS/Wordmark_SingletrackSlayer.png"}],"bikeFrames":[{"name":"Ventum One","id":2454550},{"name":"Liv LivLangma2021","id":57842352},{"name":"Giant Propel Advanced SL Disc","id":103914490},{"name":"Cervelo R5","id":106535518},{"name":"Cervelo PX-Series","id":123106780},{"name":"Trek Emonda","id":142926447},{"name":"Specialized Roubaix","id":166263359},{"name":"DiamondBack Andean","id":206931035},{"name":"Zwift Skeletal","id":270803031},{"name":"Trek Speed Concept 2021","id":291034584},{"name":"Zwift Mountain","id":385270250},{"name":"BMC SLR01","id":389045293},{"name":"Canyon Aeroad Team Edition","id":390579581},{"name":"Zwift Buffalo Qhubeka","id":427028396},{"name":"Pinarello Dogma F12","id":430380910},{"name":"Cannondale Super Six Evo","id":529764489},{"name":"Trek Super Caliber 2020","id":594642753},{"name":"Scott Plasma","id":601769019},{"name":"Specialized Ruby S-Works","id":785070754},{"name":"Canyon Speedmax","id":790618803},{"name":"Specialized Roubaix S-Works","id":807787291},{"name":"Pinarello F8","id":833740600},{"name":"Specialized Tarmac SL7","id":935373427},{"name":"Specialized Epic S-Works","id":940439989},{"name":"Specialized Venge","id":973848540},{"name":"Ridley Helium","id":988819017},{"name":"Parlee ESX","id":1000176255},{"name":"Zwift Steel","id":1029279076},{"name":"Specialized SpecializedDiverge2022","id":1133663232},{"name":"Chapter2 Chapter2TOA2021","id":1174020494},{"name":"Specialized Tarmac SL7 Sram","id":1188190925},{"name":"Scott Spark RC","id":1254205148},{"name":"Scott Foil","id":1315158373},{"name":"Canyon Grail","id":1381140630},{"name":"Specialized Shiv S-Works","id":1418783338},{"name":"Felt IA","id":1433973142},{"name":"Zwift DefaultOrange","id":1444415023},{"name":"Zwift Concept Z1","id":1456463855},{"name":"Canyon Aeroad 2015","id":1520594784},{"name":"Cannondale EVO","id":1532698216},{"name":"Canyon Lux","id":1592822481},{"name":"Specialized Specialized Crux 2022","id":1639102673},{"name":"Canyon CanyonUltimate2021","id":1675779900},{"name":"Cube CubeLitening2021","id":1703496698},{"name":"Canyon Inflite","id":1756027350},{"name":"Cube Cube Litening","id":1767548815},{"name":"Canyon Ultimate","id":1806040170},{"name":"Mosaic MosaicRT12022","id":1821736990},{"name":"Moots MootsVamoots2021","id":1874220070},{"name":"Trek Emonda SL","id":1928137471},{"name":"Cervelo CerveloS52021","id":1972610461},{"name":"Cannondale System Six","id":2005280203},{"name":"Zwift BigWheel-Concept","id":2029842509},{"name":"Specialized Amira","id":2044307781},{"name":"Cannondale Synapse","id":2059853947},{"name":"Specialized Tarmac Pro","id":2076241890},{"name":"Zwift Carbon","id":2106340733},{"name":"Zwift Buffalo Fahrrad","id":2130784714},{"name":"Uranium UraniumNuclear2021","id":2132445842},{"name":"Giant TCR Advanced SL","id":2205705045},{"name":"Specialized SpecializedAethos2021","id":2346116422},{"name":"Pinarello Dogma 65.1","id":2373108361},{"name":"Specialized Allez Sprint","id":2397946994},{"name":"Chapter2 Tere","id":2439776613},{"name":"Specialized Tarmac","id":2442494761},{"name":"VanRysel VanRyselEDR2021","id":2459800850},{"name":"Specialized Shiv Disc","id":2460287610},{"name":"Canyon CanyonSpeedmaxCRSLXDisc2021","id":2513788321},{"name":"Giant GiantTCRAdvancedSL","id":2583787351},{"name":"Cervelo S5","id":2650592817},{"name":"Zwift Gravel","id":2654154998},{"name":"Specialized Amira S-Works","id":2662728556},{"name":"Chapter2 Rere","id":2668672480},{"name":"Parlee RZ7","id":2699673850},{"name":"Cello Team","id":2943880629},{"name":"Specialized Tarmac Mixtape","id":2949471782},{"name":"BMC BMCRoadMachine2021","id":2985335789},{"name":"Felt AR","id":3002729519},{"name":"Merida Scultura","id":3033010663},{"name":"Trek Team","id":3078441697},{"name":"Zwift BigWheel","id":3079625256},{"name":"Pinarello Dogma F10","id":3186772618},{"name":"Ribble Endurance","id":3247466139},{"name":"Zwift Aero","id":3284970806},{"name":"Pinarello Bolide","id":3367169312},{"name":"BMC Timemachine01","id":3373750102},{"name":"Factor One","id":3469325930},{"name":"Canyon Speedmax Team Edition","id":3483005256},{"name":"Liv Langma Advanced SL","id":3495124341},{"name":"Specialized Venge S-Works","id":3523334161},{"name":"Zwift TT","id":3572756959},{"name":"Cervelo Aspero","id":3583022399},{"name":"Colnago Colnago V3RS","id":3628259811},{"name":"Felt FeltFR2022","id":3660740142},{"name":"Zwift Safety","id":3710262807},{"name":"Specialized Shiv","id":3772124007},{"name":"Lauf Lauf True Grit","id":3787085621},{"name":"Focus Izalco Max 2020","id":3867639546},{"name":"BMC BmcTeamMachine2022","id":3868468027},{"name":"Specialized Ruby","id":3914093169},{"name":"Pinarello Bolide TT","id":3920433954},{"name":"Bridgestone BridgestoneOVS2021","id":3928995152},{"name":"Cervelo P5","id":3932292289},{"name":"Cannondale CAAD12","id":3943092814},{"name":"Canyon Aeroad 2021","id":3985977100},{"name":"Cube Aerium","id":3998075483},{"name":"Cervelo S3D","id":4048415486},{"name":"Scott ScottAddict2021","id":4100131524},{"name":"Trek Madone","id":4129467727},{"name":"Zwift Bat","id":4150853780},{"name":"Specialized Allez","id":4200057616},{"name":"Pinarello Dogma F","id":4208139356},{"name":"Ridley Noah Fast 2019","id":4288910569}],"gameInfoHash":"B785C951B220F077DE177ECEC45C5289"} \ No newline at end of file diff --git a/online_sync.py b/online_sync.py index df1be4a..9f68171 100755 --- a/online_sync.py +++ b/online_sync.py @@ -156,7 +156,7 @@ def get_player_id(session, access_token): }, ) - profile = profile_pb2.Profile() + profile = profile_pb2.PlayerProfile() profile.ParseFromString(response.content) return profile.id diff --git a/protobuf/activity.proto b/protobuf/activity.proto index e194db9..a42c91f 100644 --- a/protobuf/activity.proto +++ b/protobuf/activity.proto @@ -1,34 +1,199 @@ syntax = "proto2"; -message Activity { +import "profile.proto"; //enum ActivityPrivacyType, Sport +//All decompiled. TODO: uncomment all new fields and use in algo +enum NotableMomentTypeZCA { + NMTC_ACHIEVEMENT_UNLOCKED = 1; + NMTC_UNLOCKED_ITEM = 2; + NMTC_MISSION_COMPLETED = 3; + NMTC_FINISHED_CHALLENGE = 4; + NMTC_TOOK_ARCH_JERSEY = 5; + NMTC_NEW_PR = 6; + NMTC_MET_DAILY_TARGET = 7; + NMTC_GAINED_LEVEL = 8; + NMTC_COMPLETED_GOAL = 9; + NMTC_FINISHED_EVENT = 10; + NMTC_FINISHED_WORKOUT = 11; + NMTC_RIDE_ON = 12; + NMTC_TRAINING_PLAN_COMPLETED = 13; +} +enum NotableMomentTypeZG_idx { + NMTI_UNKNOWN = 0; + NMTI_NEW_PR = 1; + NMTI_GAINED_LEVEL = 2; + NMTI_TRAINING_PLAN_COMPLETE = 3; + NMTI_UNLOCKED_ITEM = 4; + NMTI_ACHIEVEMENT_UNLOCKED = 5; + NMTI_MISSION_COMPLETED = 6; + NMTI_COMPLETED_GOAL = 7; + NMTI_MET_DAILY_TARGET = 8; + NMTI_TOOK_ARCH_JERSEY = 9; + NMTI_FINISHED_CHALLENGE = 10; + NMTI_FINISHED_EVENT = 11; + NMTI_FINISHED_WORKOUT = 12; + NMTI_ACTIVITY_BESTS = 13; + NMTI_RIDEON = 14; + NMTI_RIDEON_INT = 15; //international + NMTI_QUIT_EVENT = 16; + NMTI_USED_POWERUP = 17; + NMTI_PASSED_TIMING_ARCH = 18; + NMTI_CREATED_GOAL = 19; + NMTI_JOINED_EVENT = 20; + NMTI_STARTED_WORKOUT = 21; + NMTI_STARTED_MISSION = 22; + NMTI_HOLIDAY_EVENT_COMPLETE = 23; +} +enum NotableMomentTypeZG { + NMT_NEW_PR = 0; + NMT_GAINED_LEVEL = 5; + NMT_TRAINING_PLAN_COMPLETE = 19; + NMT_UNLOCKED_ITEM = 4; + NMT_ACHIEVEMENT_UNLOCKED = 2; + NMT_MISSION_COMPLETED = 3; + NMT_COMPLETED_GOAL = 10; + NMT_MET_DAILY_TARGET = 1; + NMT_TOOK_ARCH_JERSEY = 8; + NMT_FINISHED_CHALLENGE = 17; + NMT_FINISHED_EVENT = 13; + NMT_FINISHED_WORKOUT = 15; + NMT_ACTIVITY_BESTS = 20; + NMT_RIDEON = 18; + NMT_RIDEON_INT = 22; //international + NMT_QUIT_EVENT = 12; + NMT_USED_POWERUP = 6; + NMT_PASSED_TIMING_ARCH = 7; + NMT_CREATED_GOAL = 9; + NMT_JOINED_EVENT = 11; + NMT_STARTED_WORKOUT = 14; + NMT_STARTED_MISSION = 16; + NMT_HOLIDAY_EVENT_COMPLETE = 21; +} +message NotableMoment { + optional uint64 activity_id = 1; + optional NotableMomentTypeZG type = 2; + optional uint32 priority = 3; + optional uint64 incidentTime = 4; + optional string aux1 = 5; // example: {\"achievementId\":35,\"name\":\"PAIRED\",\"description\":\"Paired a phone through Zwift Companion\"} + optional string aux2 = 6; // empty string + optional string largeImageUrl = 7; +} + +message SocialInteraction { + optional uint64 player_id = 1; + optional uint32 timeDuration = 2; + optional float proximityTimeScore = 3; + optional string si_f4 = 4; +} + +message ClubAttribution { + optional string name = 1; + optional float value = 2; +} + +enum ProfileFollowStatus { + PFS_UNKNOWN = 1; + PFS_REQUESTS_TO_FOLLOW = 2; + PFS_IS_FOLLOWING = 3; + PFS_IS_BLOCKED = 4; + PFS_NO_RELATIONSHIP = 5; + PFS_SELF = 6; + PFS_HAS_BEEN_DECLINED = 7; +} +enum FitnessPrivacy { + UNSET = 0; + HIDE_SENSITIVE_DATA = 1; + SAME_AS_ACTIVITY = 2; +} + +message ActivityFull { //where is primaryImageUrl, feedImageThumbnailUrl, activityRideOnCount, activityCommentCount, eventId, rideOnGiven optional uint64 id = 1; required uint64 player_id = 2; - required uint32 f3 = 3; /* world_id or player_type_id */ + required uint64 course_id = 3; required string name = 4; - optional uint32 f5 = 5; - - optional uint32 f6 = 6; + optional string f5 = 5; + optional bool privateActivity = 6; required string start_date = 7; optional string end_date = 8; - optional float distance = 9; /* in meters */ + optional float distanceInMeters = 9; optional float avg_heart_rate = 10; optional float max_heart_rate = 11; optional float avg_watts = 12; optional float max_watts = 13; optional float avg_cadence = 14; optional float max_cadence = 15; - optional float avg_speed = 16; /* in m/s */ - optional float max_speed = 17; /* in m/s */ + optional float avg_speed = 16; // in m/s + optional float max_speed = 17; // in m/s optional float calories = 18; optional float total_elevation = 19; - optional uint64 strava_upload_id = 20; - optional uint64 strava_activity_id = 21; - optional uint32 f23 = 23; + optional uint32 strava_upload_id = 20; //uint64 stored as int32 + optional uint32 strava_activity_id = 21; //uint64 stored as int32 + optional string f22 = 22; + optional uint32 f23 = 23; //empty; stored as int32; enum up to 5 - ProfileFollowStatus? optional bytes fit = 24; optional string fit_filename = 25; - optional uint32 f29 = 29; + optional uint64 subgroupId = 26; + optional uint64 workoutHash = 27; + optional float progressPercentage = 28; + optional Sport sport = 29; + repeated string act_f30 = 30; optional string date = 31; + optional float act_f32 = 32; + optional string act_f33 = 33; + optional string act_f34 = 34; + repeated NotableMoment notables = 35; + repeated SocialInteraction socials = 36; + optional ActivityPrivacyType privacy = 37; + optional FitnessPrivacy fitness_privacy = 38; + optional string club_name = 39; + optional int64 movingTimeInMs = 40; + repeated ClubAttribution cas = 41; +} +message Activity { //field names pinned to db + optional uint64 id = 1; + required uint64 player_id = 2; + required uint64 f3 = 3; //-> rename to course_id + required string name = 4; + optional string f5 = 5; + optional bool f6 = 6; + required string start_date = 7; + optional string end_date = 8; + optional float distance = 9; // in meters + optional float avg_heart_rate = 10; + optional float max_heart_rate = 11; + optional float avg_watts = 12; + optional float max_watts = 13; + optional float avg_cadence = 14; + optional float max_cadence = 15; + optional float avg_speed = 16; // in m/s + optional float max_speed = 17; // in m/s + optional float calories = 18; + optional float total_elevation = 19; + optional uint32 strava_upload_id = 20; //uint64 stored as int32 + optional uint32 strava_activity_id = 21; //uint64 stored as int32 + //optional string f22 = 22; + optional uint32 f23 = 23; //empty; stored as int32; enum up to 5 - ProfileFollowStatus? + optional bytes fit = 24; + optional string fit_filename = 25; + //optional uint64 subgroupId = 26; + //optional uint64 workoutHash = 27; + //optional float progressPercentage = 28; + optional int64 f29 = 29; //-> Sport sport + //repeated string act_f30 = 30; + optional string date = 31; + /*optional float act_f32 = 32; + optional string act_f33 = 33; + optional string act_f34 = 34; + repeated NotableMoment notables = 35; + repeated SocialInteraction socials = 36; + optional ActivityPrivacyType privacy = 37; + optional FitnessPrivacy fitness_privacy = 38; + optional string club_name = 39; + optional int64 moving_time_ms = 40; + repeated ClubAttribution cas = 41;*/ } -message Activities { +message ActivityList { repeated Activity activities = 1; } +message ActivityListFull { + repeated ActivityFull activities = 1; +} diff --git a/protobuf/activity_pb2.py b/protobuf/activity_pb2.py index 7a14fdc..07e6bb9 100644 --- a/protobuf/activity_pb2.py +++ b/protobuf/activity_pb2.py @@ -2,6 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # source: activity.proto """Generated protocol buffer code.""" +from google.protobuf.internal import enum_type_wrapper from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import message as _message @@ -12,14 +13,128 @@ from google.protobuf import symbol_database as _symbol_database _sym_db = _symbol_database.Default() +import profile_pb2 as profile__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0e\x61\x63tivity.proto\"\xe5\x03\n\x08\x41\x63tivity\x12\n\n\x02id\x18\x01 \x01(\x04\x12\x11\n\tplayer_id\x18\x02 \x02(\x04\x12\n\n\x02\x66\x33\x18\x03 \x02(\r\x12\x0c\n\x04name\x18\x04 \x02(\t\x12\n\n\x02\x66\x35\x18\x05 \x01(\r\x12\n\n\x02\x66\x36\x18\x06 \x01(\r\x12\x12\n\nstart_date\x18\x07 \x02(\t\x12\x10\n\x08\x65nd_date\x18\x08 \x01(\t\x12\x10\n\x08\x64istance\x18\t \x01(\x02\x12\x16\n\x0e\x61vg_heart_rate\x18\n \x01(\x02\x12\x16\n\x0emax_heart_rate\x18\x0b \x01(\x02\x12\x11\n\tavg_watts\x18\x0c \x01(\x02\x12\x11\n\tmax_watts\x18\r \x01(\x02\x12\x13\n\x0b\x61vg_cadence\x18\x0e \x01(\x02\x12\x13\n\x0bmax_cadence\x18\x0f \x01(\x02\x12\x11\n\tavg_speed\x18\x10 \x01(\x02\x12\x11\n\tmax_speed\x18\x11 \x01(\x02\x12\x10\n\x08\x63\x61lories\x18\x12 \x01(\x02\x12\x17\n\x0ftotal_elevation\x18\x13 \x01(\x02\x12\x18\n\x10strava_upload_id\x18\x14 \x01(\x04\x12\x1a\n\x12strava_activity_id\x18\x15 \x01(\x04\x12\x0b\n\x03\x66\x32\x33\x18\x17 \x01(\r\x12\x0b\n\x03\x66it\x18\x18 \x01(\x0c\x12\x14\n\x0c\x66it_filename\x18\x19 \x01(\t\x12\x0b\n\x03\x66\x32\x39\x18\x1d \x01(\r\x12\x0c\n\x04\x64\x61te\x18\x1f \x01(\t\"+\n\nActivities\x12\x1d\n\nactivities\x18\x01 \x03(\x0b\x32\t.Activity') - +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0e\x61\x63tivity.proto\x1a\rprofile.proto\"\xa3\x01\n\rNotableMoment\x12\x13\n\x0b\x61\x63tivity_id\x18\x01 \x01(\x04\x12\"\n\x04type\x18\x02 \x01(\x0e\x32\x14.NotableMomentTypeZG\x12\x10\n\x08priority\x18\x03 \x01(\r\x12\x14\n\x0cincidentTime\x18\x04 \x01(\x04\x12\x0c\n\x04\x61ux1\x18\x05 \x01(\t\x12\x0c\n\x04\x61ux2\x18\x06 \x01(\t\x12\x15\n\rlargeImageUrl\x18\x07 \x01(\t\"g\n\x11SocialInteraction\x12\x11\n\tplayer_id\x18\x01 \x01(\x04\x12\x14\n\x0ctimeDuration\x18\x02 \x01(\r\x12\x1a\n\x12proximityTimeScore\x18\x03 \x01(\x02\x12\r\n\x05si_f4\x18\x04 \x01(\t\".\n\x0f\x43lubAttribution\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x02\"\x87\x07\n\x0c\x41\x63tivityFull\x12\n\n\x02id\x18\x01 \x01(\x04\x12\x11\n\tplayer_id\x18\x02 \x02(\x04\x12\x11\n\tcourse_id\x18\x03 \x02(\x04\x12\x0c\n\x04name\x18\x04 \x02(\t\x12\n\n\x02\x66\x35\x18\x05 \x01(\t\x12\x17\n\x0fprivateActivity\x18\x06 \x01(\x08\x12\x12\n\nstart_date\x18\x07 \x02(\t\x12\x10\n\x08\x65nd_date\x18\x08 \x01(\t\x12\x18\n\x10\x64istanceInMeters\x18\t \x01(\x02\x12\x16\n\x0e\x61vg_heart_rate\x18\n \x01(\x02\x12\x16\n\x0emax_heart_rate\x18\x0b \x01(\x02\x12\x11\n\tavg_watts\x18\x0c \x01(\x02\x12\x11\n\tmax_watts\x18\r \x01(\x02\x12\x13\n\x0b\x61vg_cadence\x18\x0e \x01(\x02\x12\x13\n\x0bmax_cadence\x18\x0f \x01(\x02\x12\x11\n\tavg_speed\x18\x10 \x01(\x02\x12\x11\n\tmax_speed\x18\x11 \x01(\x02\x12\x10\n\x08\x63\x61lories\x18\x12 \x01(\x02\x12\x17\n\x0ftotal_elevation\x18\x13 \x01(\x02\x12\x18\n\x10strava_upload_id\x18\x14 \x01(\r\x12\x1a\n\x12strava_activity_id\x18\x15 \x01(\r\x12\x0b\n\x03\x66\x32\x32\x18\x16 \x01(\t\x12\x0b\n\x03\x66\x32\x33\x18\x17 \x01(\r\x12\x0b\n\x03\x66it\x18\x18 \x01(\x0c\x12\x14\n\x0c\x66it_filename\x18\x19 \x01(\t\x12\x12\n\nsubgroupId\x18\x1a \x01(\x04\x12\x13\n\x0bworkoutHash\x18\x1b \x01(\x04\x12\x1a\n\x12progressPercentage\x18\x1c \x01(\x02\x12\x15\n\x05sport\x18\x1d \x01(\x0e\x32\x06.Sport\x12\x0f\n\x07\x61\x63t_f30\x18\x1e \x03(\t\x12\x0c\n\x04\x64\x61te\x18\x1f \x01(\t\x12\x0f\n\x07\x61\x63t_f32\x18 \x01(\x02\x12\x0f\n\x07\x61\x63t_f33\x18! \x01(\t\x12\x0f\n\x07\x61\x63t_f34\x18\" \x01(\t\x12 \n\x08notables\x18# \x03(\x0b\x32\x0e.NotableMoment\x12#\n\x07socials\x18$ \x03(\x0b\x32\x12.SocialInteraction\x12%\n\x07privacy\x18% \x01(\x0e\x32\x14.ActivityPrivacyType\x12(\n\x0f\x66itness_privacy\x18& \x01(\x0e\x32\x0f.FitnessPrivacy\x12\x11\n\tclub_name\x18\' \x01(\t\x12\x16\n\x0emovingTimeInMs\x18( \x01(\x03\x12\x1d\n\x03\x63\x61s\x18) \x03(\x0b\x32\x10.ClubAttribution\"\xe5\x03\n\x08\x41\x63tivity\x12\n\n\x02id\x18\x01 \x01(\x04\x12\x11\n\tplayer_id\x18\x02 \x02(\x04\x12\n\n\x02\x66\x33\x18\x03 \x02(\x04\x12\x0c\n\x04name\x18\x04 \x02(\t\x12\n\n\x02\x66\x35\x18\x05 \x01(\t\x12\n\n\x02\x66\x36\x18\x06 \x01(\x08\x12\x12\n\nstart_date\x18\x07 \x02(\t\x12\x10\n\x08\x65nd_date\x18\x08 \x01(\t\x12\x10\n\x08\x64istance\x18\t \x01(\x02\x12\x16\n\x0e\x61vg_heart_rate\x18\n \x01(\x02\x12\x16\n\x0emax_heart_rate\x18\x0b \x01(\x02\x12\x11\n\tavg_watts\x18\x0c \x01(\x02\x12\x11\n\tmax_watts\x18\r \x01(\x02\x12\x13\n\x0b\x61vg_cadence\x18\x0e \x01(\x02\x12\x13\n\x0bmax_cadence\x18\x0f \x01(\x02\x12\x11\n\tavg_speed\x18\x10 \x01(\x02\x12\x11\n\tmax_speed\x18\x11 \x01(\x02\x12\x10\n\x08\x63\x61lories\x18\x12 \x01(\x02\x12\x17\n\x0ftotal_elevation\x18\x13 \x01(\x02\x12\x18\n\x10strava_upload_id\x18\x14 \x01(\r\x12\x1a\n\x12strava_activity_id\x18\x15 \x01(\r\x12\x0b\n\x03\x66\x32\x33\x18\x17 \x01(\r\x12\x0b\n\x03\x66it\x18\x18 \x01(\x0c\x12\x14\n\x0c\x66it_filename\x18\x19 \x01(\t\x12\x0b\n\x03\x66\x32\x39\x18\x1d \x01(\x03\x12\x0c\n\x04\x64\x61te\x18\x1f \x01(\t\"-\n\x0c\x41\x63tivityList\x12\x1d\n\nactivities\x18\x01 \x03(\x0b\x32\t.Activity\"5\n\x10\x41\x63tivityListFull\x12!\n\nactivities\x18\x01 \x03(\x0b\x32\r.ActivityFull*\xe5\x02\n\x14NotableMomentTypeZCA\x12\x1d\n\x19NMTC_ACHIEVEMENT_UNLOCKED\x10\x01\x12\x16\n\x12NMTC_UNLOCKED_ITEM\x10\x02\x12\x1a\n\x16NMTC_MISSION_COMPLETED\x10\x03\x12\x1b\n\x17NMTC_FINISHED_CHALLENGE\x10\x04\x12\x19\n\x15NMTC_TOOK_ARCH_JERSEY\x10\x05\x12\x0f\n\x0bNMTC_NEW_PR\x10\x06\x12\x19\n\x15NMTC_MET_DAILY_TARGET\x10\x07\x12\x15\n\x11NMTC_GAINED_LEVEL\x10\x08\x12\x17\n\x13NMTC_COMPLETED_GOAL\x10\t\x12\x17\n\x13NMTC_FINISHED_EVENT\x10\n\x12\x19\n\x15NMTC_FINISHED_WORKOUT\x10\x0b\x12\x10\n\x0cNMTC_RIDE_ON\x10\x0c\x12 \n\x1cNMTC_TRAINING_PLAN_COMPLETED\x10\r*\xf2\x04\n\x17NotableMomentTypeZG_idx\x12\x10\n\x0cNMTI_UNKNOWN\x10\x00\x12\x0f\n\x0bNMTI_NEW_PR\x10\x01\x12\x15\n\x11NMTI_GAINED_LEVEL\x10\x02\x12\x1f\n\x1bNMTI_TRAINING_PLAN_COMPLETE\x10\x03\x12\x16\n\x12NMTI_UNLOCKED_ITEM\x10\x04\x12\x1d\n\x19NMTI_ACHIEVEMENT_UNLOCKED\x10\x05\x12\x1a\n\x16NMTI_MISSION_COMPLETED\x10\x06\x12\x17\n\x13NMTI_COMPLETED_GOAL\x10\x07\x12\x19\n\x15NMTI_MET_DAILY_TARGET\x10\x08\x12\x19\n\x15NMTI_TOOK_ARCH_JERSEY\x10\t\x12\x1b\n\x17NMTI_FINISHED_CHALLENGE\x10\n\x12\x17\n\x13NMTI_FINISHED_EVENT\x10\x0b\x12\x19\n\x15NMTI_FINISHED_WORKOUT\x10\x0c\x12\x17\n\x13NMTI_ACTIVITY_BESTS\x10\r\x12\x0f\n\x0bNMTI_RIDEON\x10\x0e\x12\x13\n\x0fNMTI_RIDEON_INT\x10\x0f\x12\x13\n\x0fNMTI_QUIT_EVENT\x10\x10\x12\x15\n\x11NMTI_USED_POWERUP\x10\x11\x12\x1b\n\x17NMTI_PASSED_TIMING_ARCH\x10\x12\x12\x15\n\x11NMTI_CREATED_GOAL\x10\x13\x12\x15\n\x11NMTI_JOINED_EVENT\x10\x14\x12\x18\n\x14NMTI_STARTED_WORKOUT\x10\x15\x12\x18\n\x14NMTI_STARTED_MISSION\x10\x16\x12\x1f\n\x1bNMTI_HOLIDAY_EVENT_COMPLETE\x10\x17*\xc5\x04\n\x13NotableMomentTypeZG\x12\x0e\n\nNMT_NEW_PR\x10\x00\x12\x14\n\x10NMT_GAINED_LEVEL\x10\x05\x12\x1e\n\x1aNMT_TRAINING_PLAN_COMPLETE\x10\x13\x12\x15\n\x11NMT_UNLOCKED_ITEM\x10\x04\x12\x1c\n\x18NMT_ACHIEVEMENT_UNLOCKED\x10\x02\x12\x19\n\x15NMT_MISSION_COMPLETED\x10\x03\x12\x16\n\x12NMT_COMPLETED_GOAL\x10\n\x12\x18\n\x14NMT_MET_DAILY_TARGET\x10\x01\x12\x18\n\x14NMT_TOOK_ARCH_JERSEY\x10\x08\x12\x1a\n\x16NMT_FINISHED_CHALLENGE\x10\x11\x12\x16\n\x12NMT_FINISHED_EVENT\x10\r\x12\x18\n\x14NMT_FINISHED_WORKOUT\x10\x0f\x12\x16\n\x12NMT_ACTIVITY_BESTS\x10\x14\x12\x0e\n\nNMT_RIDEON\x10\x12\x12\x12\n\x0eNMT_RIDEON_INT\x10\x16\x12\x12\n\x0eNMT_QUIT_EVENT\x10\x0c\x12\x14\n\x10NMT_USED_POWERUP\x10\x06\x12\x1a\n\x16NMT_PASSED_TIMING_ARCH\x10\x07\x12\x14\n\x10NMT_CREATED_GOAL\x10\t\x12\x14\n\x10NMT_JOINED_EVENT\x10\x0b\x12\x17\n\x13NMT_STARTED_WORKOUT\x10\x0e\x12\x17\n\x13NMT_STARTED_MISSION\x10\x10\x12\x1e\n\x1aNMT_HOLIDAY_EVENT_COMPLETE\x10\x15*\xae\x01\n\x13ProfileFollowStatus\x12\x0f\n\x0bPFS_UNKNOWN\x10\x01\x12\x1a\n\x16PFS_REQUESTS_TO_FOLLOW\x10\x02\x12\x14\n\x10PFS_IS_FOLLOWING\x10\x03\x12\x12\n\x0ePFS_IS_BLOCKED\x10\x04\x12\x17\n\x13PFS_NO_RELATIONSHIP\x10\x05\x12\x0c\n\x08PFS_SELF\x10\x06\x12\x19\n\x15PFS_HAS_BEEN_DECLINED\x10\x07*J\n\x0e\x46itnessPrivacy\x12\t\n\x05UNSET\x10\x00\x12\x17\n\x13HIDE_SENSITIVE_DATA\x10\x01\x12\x14\n\x10SAME_AS_ACTIVITY\x10\x02') + +_NOTABLEMOMENTTYPEZCA = DESCRIPTOR.enum_types_by_name['NotableMomentTypeZCA'] +NotableMomentTypeZCA = enum_type_wrapper.EnumTypeWrapper(_NOTABLEMOMENTTYPEZCA) +_NOTABLEMOMENTTYPEZG_IDX = DESCRIPTOR.enum_types_by_name['NotableMomentTypeZG_idx'] +NotableMomentTypeZG_idx = enum_type_wrapper.EnumTypeWrapper(_NOTABLEMOMENTTYPEZG_IDX) +_NOTABLEMOMENTTYPEZG = DESCRIPTOR.enum_types_by_name['NotableMomentTypeZG'] +NotableMomentTypeZG = enum_type_wrapper.EnumTypeWrapper(_NOTABLEMOMENTTYPEZG) +_PROFILEFOLLOWSTATUS = DESCRIPTOR.enum_types_by_name['ProfileFollowStatus'] +ProfileFollowStatus = enum_type_wrapper.EnumTypeWrapper(_PROFILEFOLLOWSTATUS) +_FITNESSPRIVACY = DESCRIPTOR.enum_types_by_name['FitnessPrivacy'] +FitnessPrivacy = enum_type_wrapper.EnumTypeWrapper(_FITNESSPRIVACY) +NMTC_ACHIEVEMENT_UNLOCKED = 1 +NMTC_UNLOCKED_ITEM = 2 +NMTC_MISSION_COMPLETED = 3 +NMTC_FINISHED_CHALLENGE = 4 +NMTC_TOOK_ARCH_JERSEY = 5 +NMTC_NEW_PR = 6 +NMTC_MET_DAILY_TARGET = 7 +NMTC_GAINED_LEVEL = 8 +NMTC_COMPLETED_GOAL = 9 +NMTC_FINISHED_EVENT = 10 +NMTC_FINISHED_WORKOUT = 11 +NMTC_RIDE_ON = 12 +NMTC_TRAINING_PLAN_COMPLETED = 13 +NMTI_UNKNOWN = 0 +NMTI_NEW_PR = 1 +NMTI_GAINED_LEVEL = 2 +NMTI_TRAINING_PLAN_COMPLETE = 3 +NMTI_UNLOCKED_ITEM = 4 +NMTI_ACHIEVEMENT_UNLOCKED = 5 +NMTI_MISSION_COMPLETED = 6 +NMTI_COMPLETED_GOAL = 7 +NMTI_MET_DAILY_TARGET = 8 +NMTI_TOOK_ARCH_JERSEY = 9 +NMTI_FINISHED_CHALLENGE = 10 +NMTI_FINISHED_EVENT = 11 +NMTI_FINISHED_WORKOUT = 12 +NMTI_ACTIVITY_BESTS = 13 +NMTI_RIDEON = 14 +NMTI_RIDEON_INT = 15 +NMTI_QUIT_EVENT = 16 +NMTI_USED_POWERUP = 17 +NMTI_PASSED_TIMING_ARCH = 18 +NMTI_CREATED_GOAL = 19 +NMTI_JOINED_EVENT = 20 +NMTI_STARTED_WORKOUT = 21 +NMTI_STARTED_MISSION = 22 +NMTI_HOLIDAY_EVENT_COMPLETE = 23 +NMT_NEW_PR = 0 +NMT_GAINED_LEVEL = 5 +NMT_TRAINING_PLAN_COMPLETE = 19 +NMT_UNLOCKED_ITEM = 4 +NMT_ACHIEVEMENT_UNLOCKED = 2 +NMT_MISSION_COMPLETED = 3 +NMT_COMPLETED_GOAL = 10 +NMT_MET_DAILY_TARGET = 1 +NMT_TOOK_ARCH_JERSEY = 8 +NMT_FINISHED_CHALLENGE = 17 +NMT_FINISHED_EVENT = 13 +NMT_FINISHED_WORKOUT = 15 +NMT_ACTIVITY_BESTS = 20 +NMT_RIDEON = 18 +NMT_RIDEON_INT = 22 +NMT_QUIT_EVENT = 12 +NMT_USED_POWERUP = 6 +NMT_PASSED_TIMING_ARCH = 7 +NMT_CREATED_GOAL = 9 +NMT_JOINED_EVENT = 11 +NMT_STARTED_WORKOUT = 14 +NMT_STARTED_MISSION = 16 +NMT_HOLIDAY_EVENT_COMPLETE = 21 +PFS_UNKNOWN = 1 +PFS_REQUESTS_TO_FOLLOW = 2 +PFS_IS_FOLLOWING = 3 +PFS_IS_BLOCKED = 4 +PFS_NO_RELATIONSHIP = 5 +PFS_SELF = 6 +PFS_HAS_BEEN_DECLINED = 7 +UNSET = 0 +HIDE_SENSITIVE_DATA = 1 +SAME_AS_ACTIVITY = 2 +_NOTABLEMOMENT = DESCRIPTOR.message_types_by_name['NotableMoment'] +_SOCIALINTERACTION = DESCRIPTOR.message_types_by_name['SocialInteraction'] +_CLUBATTRIBUTION = DESCRIPTOR.message_types_by_name['ClubAttribution'] +_ACTIVITYFULL = DESCRIPTOR.message_types_by_name['ActivityFull'] _ACTIVITY = DESCRIPTOR.message_types_by_name['Activity'] -_ACTIVITIES = DESCRIPTOR.message_types_by_name['Activities'] +_ACTIVITYLIST = DESCRIPTOR.message_types_by_name['ActivityList'] +_ACTIVITYLISTFULL = DESCRIPTOR.message_types_by_name['ActivityListFull'] +NotableMoment = _reflection.GeneratedProtocolMessageType('NotableMoment', (_message.Message,), { + 'DESCRIPTOR' : _NOTABLEMOMENT, + '__module__' : 'activity_pb2' + # @@protoc_insertion_point(class_scope:NotableMoment) + }) +_sym_db.RegisterMessage(NotableMoment) + +SocialInteraction = _reflection.GeneratedProtocolMessageType('SocialInteraction', (_message.Message,), { + 'DESCRIPTOR' : _SOCIALINTERACTION, + '__module__' : 'activity_pb2' + # @@protoc_insertion_point(class_scope:SocialInteraction) + }) +_sym_db.RegisterMessage(SocialInteraction) + +ClubAttribution = _reflection.GeneratedProtocolMessageType('ClubAttribution', (_message.Message,), { + 'DESCRIPTOR' : _CLUBATTRIBUTION, + '__module__' : 'activity_pb2' + # @@protoc_insertion_point(class_scope:ClubAttribution) + }) +_sym_db.RegisterMessage(ClubAttribution) + +ActivityFull = _reflection.GeneratedProtocolMessageType('ActivityFull', (_message.Message,), { + 'DESCRIPTOR' : _ACTIVITYFULL, + '__module__' : 'activity_pb2' + # @@protoc_insertion_point(class_scope:ActivityFull) + }) +_sym_db.RegisterMessage(ActivityFull) + Activity = _reflection.GeneratedProtocolMessageType('Activity', (_message.Message,), { 'DESCRIPTOR' : _ACTIVITY, '__module__' : 'activity_pb2' @@ -27,18 +142,45 @@ Activity = _reflection.GeneratedProtocolMessageType('Activity', (_message.Messag }) _sym_db.RegisterMessage(Activity) -Activities = _reflection.GeneratedProtocolMessageType('Activities', (_message.Message,), { - 'DESCRIPTOR' : _ACTIVITIES, +ActivityList = _reflection.GeneratedProtocolMessageType('ActivityList', (_message.Message,), { + 'DESCRIPTOR' : _ACTIVITYLIST, '__module__' : 'activity_pb2' - # @@protoc_insertion_point(class_scope:Activities) + # @@protoc_insertion_point(class_scope:ActivityList) }) -_sym_db.RegisterMessage(Activities) +_sym_db.RegisterMessage(ActivityList) + +ActivityListFull = _reflection.GeneratedProtocolMessageType('ActivityListFull', (_message.Message,), { + 'DESCRIPTOR' : _ACTIVITYLISTFULL, + '__module__' : 'activity_pb2' + # @@protoc_insertion_point(class_scope:ActivityListFull) + }) +_sym_db.RegisterMessage(ActivityListFull) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None - _ACTIVITY._serialized_start=19 - _ACTIVITY._serialized_end=504 - _ACTIVITIES._serialized_start=506 - _ACTIVITIES._serialized_end=549 + _NOTABLEMOMENTTYPEZCA._serialized_start=1849 + _NOTABLEMOMENTTYPEZCA._serialized_end=2206 + _NOTABLEMOMENTTYPEZG_IDX._serialized_start=2209 + _NOTABLEMOMENTTYPEZG_IDX._serialized_end=2835 + _NOTABLEMOMENTTYPEZG._serialized_start=2838 + _NOTABLEMOMENTTYPEZG._serialized_end=3419 + _PROFILEFOLLOWSTATUS._serialized_start=3422 + _PROFILEFOLLOWSTATUS._serialized_end=3596 + _FITNESSPRIVACY._serialized_start=3598 + _FITNESSPRIVACY._serialized_end=3672 + _NOTABLEMOMENT._serialized_start=34 + _NOTABLEMOMENT._serialized_end=197 + _SOCIALINTERACTION._serialized_start=199 + _SOCIALINTERACTION._serialized_end=302 + _CLUBATTRIBUTION._serialized_start=304 + _CLUBATTRIBUTION._serialized_end=350 + _ACTIVITYFULL._serialized_start=353 + _ACTIVITYFULL._serialized_end=1256 + _ACTIVITY._serialized_start=1259 + _ACTIVITY._serialized_end=1744 + _ACTIVITYLIST._serialized_start=1746 + _ACTIVITYLIST._serialized_end=1791 + _ACTIVITYLISTFULL._serialized_start=1793 + _ACTIVITYLISTFULL._serialized_end=1846 # @@protoc_insertion_point(module_scope) diff --git a/protobuf/events.proto b/protobuf/events.proto index d56b52a..e520cd7 100644 --- a/protobuf/events.proto +++ b/protobuf/events.proto @@ -1,13 +1,17 @@ syntax = "proto2"; +import "profile.proto"; //enums PlayerType and Sport -message Category { +message EventSubgroupProtobuf { //where is fieldLimit, signedUp, signupStatus, registered, registrationStatus, followeeEntrantCount +//totalEntrantCount, followeeSignedUpCount, totalSignedUpCount, followeeJoinedCount, totalJoinedCount, rulesSet, workoutHash, overrideMapPreferences +//qualificationRuleIds, accessValidationResult required uint64 id = 1; // 2395269 - optional string description = 2; // "3R True2 Steady Ride [2.0w/kg avg] (C)" - optional string f3 = 3; // "" - optional uint32 f5 = 5; // 154 - optional uint32 f6 = 6; // 0 - optional string f7 = 7; // "PT3600S" - optional uint32 rules_id = 8; // 320 + optional string name = 2; // ex: "3R True2 Steady Ride [2.0w/kg avg] (C)" + optional string description = 3; // ex: "Welcome to our sociable early morning/evening social group ride." + optional uint32 evs_f4 = 4; // "" + optional uint32 evs_f5 = 5; // 154 and others + optional uint32 evs_f6 = 6; // 0 + optional string scode = 7; // ex: "PT3600S" + optional uint64 rules_id = 8; // 320 and others optional uint64 registrationStart = 9; optional uint64 registrationStartWT = 10; optional uint64 registrationEnd = 11; @@ -18,58 +22,134 @@ message Category { optional uint64 lineUpEndWT = 16; optional uint64 eventSubgroupStart = 17; optional uint64 eventSubgroupStartWT = 18; - optional uint32 f21 = 21; // 0 + optional uint64 evs_f19 = 19; + optional uint64 evs_f20 = 20; //tag416 + optional bool evs_f21 = 21; // false, tag424 required uint64 route_id = 22; // 3366225080 - repeated uint32 leaders = 23; // or sweepers? - optional fixed32 f24 = 24; - optional uint32 laps = 25; // 0 - optional uint32 startLocation = 29; // 13 - optional uint32 label = 30; // 3 - optional uint32 f31 = 31; // 1 - optional fixed32 f32 = 32; // 1076258406 - optional fixed32 f33 = 33; // 1078774989 - optional uint32 duration = 34; // Duration of event in seconds - optional uint64 f36 = 36; // 493134166 - optional uint32 f37 = 37; // 0 - optional string audio = 39; // "https://cdn.zwift.com/AudioBroadcasts/wbrgrouprideaudiov4" - repeated uint32 sweepers = 41; // or leaders? - optional string f43 = 43; - optional uint32 f44 = 44; // 0 - optional string tags = 45; // semi-colon delimited tags eg: "fenced;3r;created_ryan;communityevent;no_kick_mode;timestamp=1603911177622" - optional uint32 lateJoinInMinutes = 46; - optional uint32 map_id = 47; // 1 + repeated uint64 invitedLeaders = 23; // tag440 + optional float distanceInMeters = 24; // tag453 + optional uint32 laps = 25; // tag456 + // no 26-28 + optional uint64 startLocation = 29; // 13, tag488 [>=6 -> 'bad start location'] valid values: 1..5 (0->1) + optional uint32 label = 30; // A:1, B:2, C:3, D:4, E:5 etc, tag496 + optional uint32 paceType = 31; // 1 everywhere, tag504 + optional float fromPaceValue = 32; // tag645 + optional float toPaceValue = 33; // tag653 + optional uint32 durationInSeconds = 34; // Duration of event in seconds, tag656 + optional uint32 evs_f35 = 35; // tag664 + optional uint64 jerseyHash = 36; // 493134166, tag672 + optional bool evs_f37 = 37; // 0, tag680 + optional uint32 evs_f38 = 38; // tag688 + optional string auxiliaryUrl = 39; // "https://cdn.zwift.com/AudioBroadcasts/wbrgrouprideaudiov4", tag698 + optional uint64 bikeHash = 40; // 4208139356, tag704 + repeated uint64 invitedSweepers = 41; // tag712 + optional uint64 evs_f42 = 42; // tag720 + optional string customUrl = 43; // https://cdn.zwift.com/events/upload/workouts/CafeRide1.zwo, tag730 + optional bool evs_f44 = 44; // false, tag736 + optional string tags = 45; // tag746, semi-colon delimited tags eg: "fenced;3r;created_ryan;communityevent;no_kick_mode;timestamp=1603911177622" + optional uint32 lateJoinInMinutes = 46; //tag752 + optional uint64 course_id = 47; // 1 and others: course?, tag760 + optional uint64 evs_f48 = 48; //tag898 + optional string routeUrl = 49; //tag906 + repeated int32 evs_f50 = 50; //tag912 + optional bool evs_f51 = 51; //tag920 } -message Event { +enum EventVisibility { + EV_NULL = 0; + EV_PUB_SHARE = 1; // event public shareable + EV_BY_RESOURCE = 2; // event defined by resource + EV_SHAREABLE = 3; +} +message MicroserviceEventData { + optional string name = 1; // "clubs" everywhere (json: microserviceName) + optional bytes externalResourceId = 2; // different 16-byte bb4538bfd13346c99a4df2b3cc3b5d95 (json: microserviceExternalResourceId) + optional EventVisibility visibility = 3; // enum 1 (json: microserviceEventVisibility) +} + +message EventSeriesProtobuf { //{"id":4531,"name":"Zwift Academy Triathlon - Baseline TT","description":null,"imported":false} + optional uint64 id = 1; //5445 or 1485 + optional string name = 2; //INEOSVTC or "Fast Friday" + optional string description = 3; //"" or "Congratulations, you crushed another week of workouts! ..." +} + +message EventTimeTrialOptions { //{"timeGapBetweenRowsMs":15000,"maxRows":50,"maxRidersPerRow":10} + optional uint32 timeGapBetweenRowsMs = 1; //15000 everywhere + optional uint32 maxRows = 2; //50 or 25 + optional uint32 maxRidersPerRow = 3; //10 everywhere + optional uint32 evt_f4 = 4; + optional uint64 evt_f5 = 5; +} + +enum EventTypeV2 { + EVENT_TYPE_UNKNOWN = 0; + EVENT_TYPE_EFONDO = 1; + EVENT_TYPE_RACE = 2; + EVENT_TYPE_GROUP_RIDE = 3; + EVENT_TYPE_GROUP_WORKOUT = 4; + EVENT_TYPE_TIME_TRIAL = 5; +} +enum EventType { + ET_UNKNOWN = 0; + EFONDO = 1; + RACE = 2; + GROUP_RIDE = 3; + GROUP_WORKOUT = 4; + TIME_TRIAL = 5; +} +enum EventCulling { + CULLING_UNDEFINED = 0; + CULLING_EVERYBODY = 1; + CULLING_EVENT_ONLY = 2; + CULLING_SUBGROUP_ONLY = 3; +} +message Event { //real name: EventProtobuf; where is shortName, shortDescription, rulesSet, routeUrl, bikeHash, +//privateEvent, followeeEntrantCount, totalEntrantCount, followeeSignedUpCount, totalSignedUpCount, followeeJoinedCount, +//totalJoinedCount, auxiliaryUrl, imageS3Name, imageS3Bucket, cullingType, recurring, recurringOffset, publishRecurring, parentId, type, workoutHash, +//customUrl, restricted, unlisted, eventSecret, accessExpression, qualificationRuleIds, minGameVersion, recordable, imported, eventTemplateId required uint64 id = 1; - optional uint32 world_id = 2; - required string title = 3; + optional uint64 server_realm = 2; + required string name = 3; optional string description = 4; optional uint64 eventStart = 5; // Start time (epoch time in ms) - optional fixed32 f7 = 7; - optional uint64 laps = 8; - optional uint64 f9 = 9; - repeated Category category = 10; - optional string f11 = 11; - optional string pic_url = 12; - optional uint32 duration = 13; // Duration in seconds? + optional string e_f6 = 6; + optional float distanceInMeters = 7; + optional uint32 laps = 8; + optional uint32 e_f9 = 9; + repeated EventSubgroupProtobuf category = 10; //event_subgroup_size() <= MAX_SUBGROUPS(6) + optional string e_f11 = 11; + optional string imageUrl = 12; + optional uint32 durationInSeconds = 13; optional uint64 route_id = 14; optional uint64 rules_id = 15; - optional uint64 f16 = 16; - optional uint64 f17 = 17; - optional uint64 f18 = 18; - optional string f19 = 19; - optional uint32 f22 = 22; - optional uint32 f24 = 24; - optional string f26 = 26; - optional uint32 f27 = 27; - optional uint32 f28 = 28; - optional uint32 f29 = 29; + optional uint32 e_f16 = 16; + optional bool visible = 17; + optional uint64 jerseyHash = 18; + optional string e_f19 = 19; + optional string e_f20 = 20; + optional string e_f21 = 21; + optional Sport sport = 22; + optional uint64 e_f23 = 23; + optional EventType eventType = 24; + optional uint64 e_f25 = 25; + optional string e_f26 = 26; + optional uint64 e_f27 = 27; //<=4, ENUM? + optional bool overrideMapPreferences = 28; + optional bool invisibleToNonParticipants = 29; + optional EventSeriesProtobuf evSeries = 30; optional string tags = 31; // semi-colon delimited tags + optional uint64 e_f32 = 32; + optional bool e_wtrl = 33; //WTRL (World Tactical Racing Leagues) optional uint32 lateJoinInMinutes = 34; - optional uint32 map_id = 35; + optional uint64 course_id = 35; + optional EventTimeTrialOptions tto = 36; + optional string e_f37 = 37; + optional string e_f38 = 38; + optional uint32 e_f39 = 39; + optional MicroserviceEventData msed = 40; + repeated uint32 e_f41 = 41; } -message Events { +message Events { //real name: EventsProtobuf repeated Event events = 1; } diff --git a/protobuf/events_pb2.py b/protobuf/events_pb2.py index b7aff5a..8190ab5 100644 --- a/protobuf/events_pb2.py +++ b/protobuf/events_pb2.py @@ -2,6 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # source: events.proto """Generated protocol buffer code.""" +from google.protobuf.internal import enum_type_wrapper from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import message as _message @@ -12,21 +13,74 @@ from google.protobuf import symbol_database as _symbol_database _sym_db = _symbol_database.Default() +import profile_pb2 as profile__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0c\x65vents.proto\"\x9f\x05\n\x08\x43\x61tegory\x12\n\n\x02id\x18\x01 \x02(\x04\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\n\n\x02\x66\x33\x18\x03 \x01(\t\x12\n\n\x02\x66\x35\x18\x05 \x01(\r\x12\n\n\x02\x66\x36\x18\x06 \x01(\r\x12\n\n\x02\x66\x37\x18\x07 \x01(\t\x12\x10\n\x08rules_id\x18\x08 \x01(\r\x12\x19\n\x11registrationStart\x18\t \x01(\x04\x12\x1b\n\x13registrationStartWT\x18\n \x01(\x04\x12\x17\n\x0fregistrationEnd\x18\x0b \x01(\x04\x12\x19\n\x11registrationEndWT\x18\x0c \x01(\x04\x12\x13\n\x0blineUpStart\x18\r \x01(\x04\x12\x15\n\rlineUpStartWT\x18\x0e \x01(\x04\x12\x11\n\tlineUpEnd\x18\x0f \x01(\x04\x12\x13\n\x0blineUpEndWT\x18\x10 \x01(\x04\x12\x1a\n\x12\x65ventSubgroupStart\x18\x11 \x01(\x04\x12\x1c\n\x14\x65ventSubgroupStartWT\x18\x12 \x01(\x04\x12\x0b\n\x03\x66\x32\x31\x18\x15 \x01(\r\x12\x10\n\x08route_id\x18\x16 \x02(\x04\x12\x0f\n\x07leaders\x18\x17 \x03(\r\x12\x0b\n\x03\x66\x32\x34\x18\x18 \x01(\x07\x12\x0c\n\x04laps\x18\x19 \x01(\r\x12\x15\n\rstartLocation\x18\x1d \x01(\r\x12\r\n\x05label\x18\x1e \x01(\r\x12\x0b\n\x03\x66\x33\x31\x18\x1f \x01(\r\x12\x0b\n\x03\x66\x33\x32\x18 \x01(\x07\x12\x0b\n\x03\x66\x33\x33\x18! \x01(\x07\x12\x10\n\x08\x64uration\x18\" \x01(\r\x12\x0b\n\x03\x66\x33\x36\x18$ \x01(\x04\x12\x0b\n\x03\x66\x33\x37\x18% \x01(\r\x12\r\n\x05\x61udio\x18\' \x01(\t\x12\x10\n\x08sweepers\x18) \x03(\r\x12\x0b\n\x03\x66\x34\x33\x18+ \x01(\t\x12\x0b\n\x03\x66\x34\x34\x18, \x01(\r\x12\x0c\n\x04tags\x18- \x01(\t\x12\x19\n\x11lateJoinInMinutes\x18. \x01(\r\x12\x0e\n\x06map_id\x18/ \x01(\r\"\xaf\x03\n\x05\x45vent\x12\n\n\x02id\x18\x01 \x02(\x04\x12\x10\n\x08world_id\x18\x02 \x01(\r\x12\r\n\x05title\x18\x03 \x02(\t\x12\x13\n\x0b\x64\x65scription\x18\x04 \x01(\t\x12\x12\n\neventStart\x18\x05 \x01(\x04\x12\n\n\x02\x66\x37\x18\x07 \x01(\x07\x12\x0c\n\x04laps\x18\x08 \x01(\x04\x12\n\n\x02\x66\x39\x18\t \x01(\x04\x12\x1b\n\x08\x63\x61tegory\x18\n \x03(\x0b\x32\t.Category\x12\x0b\n\x03\x66\x31\x31\x18\x0b \x01(\t\x12\x0f\n\x07pic_url\x18\x0c \x01(\t\x12\x10\n\x08\x64uration\x18\r \x01(\r\x12\x10\n\x08route_id\x18\x0e \x01(\x04\x12\x10\n\x08rules_id\x18\x0f \x01(\x04\x12\x0b\n\x03\x66\x31\x36\x18\x10 \x01(\x04\x12\x0b\n\x03\x66\x31\x37\x18\x11 \x01(\x04\x12\x0b\n\x03\x66\x31\x38\x18\x12 \x01(\x04\x12\x0b\n\x03\x66\x31\x39\x18\x13 \x01(\t\x12\x0b\n\x03\x66\x32\x32\x18\x16 \x01(\r\x12\x0b\n\x03\x66\x32\x34\x18\x18 \x01(\r\x12\x0b\n\x03\x66\x32\x36\x18\x1a \x01(\t\x12\x0b\n\x03\x66\x32\x37\x18\x1b \x01(\r\x12\x0b\n\x03\x66\x32\x38\x18\x1c \x01(\r\x12\x0b\n\x03\x66\x32\x39\x18\x1d \x01(\r\x12\x0c\n\x04tags\x18\x1f \x01(\t\x12\x19\n\x11lateJoinInMinutes\x18\" \x01(\r\x12\x0e\n\x06map_id\x18# \x01(\r\" \n\x06\x45vents\x12\x16\n\x06\x65vents\x18\x01 \x03(\x0b\x32\x06.Event') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0c\x65vents.proto\x1a\rprofile.proto\"\xd3\x07\n\x15\x45ventSubgroupProtobuf\x12\n\n\x02id\x18\x01 \x02(\x04\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x03 \x01(\t\x12\x0e\n\x06\x65vs_f4\x18\x04 \x01(\r\x12\x0e\n\x06\x65vs_f5\x18\x05 \x01(\r\x12\x0e\n\x06\x65vs_f6\x18\x06 \x01(\r\x12\r\n\x05scode\x18\x07 \x01(\t\x12\x10\n\x08rules_id\x18\x08 \x01(\x04\x12\x19\n\x11registrationStart\x18\t \x01(\x04\x12\x1b\n\x13registrationStartWT\x18\n \x01(\x04\x12\x17\n\x0fregistrationEnd\x18\x0b \x01(\x04\x12\x19\n\x11registrationEndWT\x18\x0c \x01(\x04\x12\x13\n\x0blineUpStart\x18\r \x01(\x04\x12\x15\n\rlineUpStartWT\x18\x0e \x01(\x04\x12\x11\n\tlineUpEnd\x18\x0f \x01(\x04\x12\x13\n\x0blineUpEndWT\x18\x10 \x01(\x04\x12\x1a\n\x12\x65ventSubgroupStart\x18\x11 \x01(\x04\x12\x1c\n\x14\x65ventSubgroupStartWT\x18\x12 \x01(\x04\x12\x0f\n\x07\x65vs_f19\x18\x13 \x01(\x04\x12\x0f\n\x07\x65vs_f20\x18\x14 \x01(\x04\x12\x0f\n\x07\x65vs_f21\x18\x15 \x01(\x08\x12\x10\n\x08route_id\x18\x16 \x02(\x04\x12\x16\n\x0einvitedLeaders\x18\x17 \x03(\x04\x12\x18\n\x10\x64istanceInMeters\x18\x18 \x01(\x02\x12\x0c\n\x04laps\x18\x19 \x01(\r\x12\x15\n\rstartLocation\x18\x1d \x01(\x04\x12\r\n\x05label\x18\x1e \x01(\r\x12\x10\n\x08paceType\x18\x1f \x01(\r\x12\x15\n\rfromPaceValue\x18 \x01(\x02\x12\x13\n\x0btoPaceValue\x18! \x01(\x02\x12\x19\n\x11\x64urationInSeconds\x18\" \x01(\r\x12\x0f\n\x07\x65vs_f35\x18# \x01(\r\x12\x12\n\njerseyHash\x18$ \x01(\x04\x12\x0f\n\x07\x65vs_f37\x18% \x01(\x08\x12\x0f\n\x07\x65vs_f38\x18& \x01(\r\x12\x14\n\x0c\x61uxiliaryUrl\x18\' \x01(\t\x12\x10\n\x08\x62ikeHash\x18( \x01(\x04\x12\x17\n\x0finvitedSweepers\x18) \x03(\x04\x12\x0f\n\x07\x65vs_f42\x18* \x01(\x04\x12\x11\n\tcustomUrl\x18+ \x01(\t\x12\x0f\n\x07\x65vs_f44\x18, \x01(\x08\x12\x0c\n\x04tags\x18- \x01(\t\x12\x19\n\x11lateJoinInMinutes\x18. \x01(\r\x12\x11\n\tcourse_id\x18/ \x01(\x04\x12\x0f\n\x07\x65vs_f48\x18\x30 \x01(\x04\x12\x10\n\x08routeUrl\x18\x31 \x01(\t\x12\x0f\n\x07\x65vs_f50\x18\x32 \x03(\x05\x12\x0f\n\x07\x65vs_f51\x18\x33 \x01(\x08\"g\n\x15MicroserviceEventData\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x1a\n\x12\x65xternalResourceId\x18\x02 \x01(\x0c\x12$\n\nvisibility\x18\x03 \x01(\x0e\x32\x10.EventVisibility\"D\n\x13\x45ventSeriesProtobuf\x12\n\n\x02id\x18\x01 \x01(\x04\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x03 \x01(\t\"\x7f\n\x15\x45ventTimeTrialOptions\x12\x1c\n\x14timeGapBetweenRowsMs\x18\x01 \x01(\r\x12\x0f\n\x07maxRows\x18\x02 \x01(\r\x12\x17\n\x0fmaxRidersPerRow\x18\x03 \x01(\r\x12\x0e\n\x06\x65vt_f4\x18\x04 \x01(\r\x12\x0e\n\x06\x65vt_f5\x18\x05 \x01(\x04\"\xcf\x06\n\x05\x45vent\x12\n\n\x02id\x18\x01 \x02(\x04\x12\x14\n\x0cserver_realm\x18\x02 \x01(\x04\x12\x0c\n\x04name\x18\x03 \x02(\t\x12\x13\n\x0b\x64\x65scription\x18\x04 \x01(\t\x12\x12\n\neventStart\x18\x05 \x01(\x04\x12\x0c\n\x04\x65_f6\x18\x06 \x01(\t\x12\x18\n\x10\x64istanceInMeters\x18\x07 \x01(\x02\x12\x0c\n\x04laps\x18\x08 \x01(\r\x12\x0c\n\x04\x65_f9\x18\t \x01(\r\x12(\n\x08\x63\x61tegory\x18\n \x03(\x0b\x32\x16.EventSubgroupProtobuf\x12\r\n\x05\x65_f11\x18\x0b \x01(\t\x12\x10\n\x08imageUrl\x18\x0c \x01(\t\x12\x19\n\x11\x64urationInSeconds\x18\r \x01(\r\x12\x10\n\x08route_id\x18\x0e \x01(\x04\x12\x10\n\x08rules_id\x18\x0f \x01(\x04\x12\r\n\x05\x65_f16\x18\x10 \x01(\r\x12\x0f\n\x07visible\x18\x11 \x01(\x08\x12\x12\n\njerseyHash\x18\x12 \x01(\x04\x12\r\n\x05\x65_f19\x18\x13 \x01(\t\x12\r\n\x05\x65_f20\x18\x14 \x01(\t\x12\r\n\x05\x65_f21\x18\x15 \x01(\t\x12\x15\n\x05sport\x18\x16 \x01(\x0e\x32\x06.Sport\x12\r\n\x05\x65_f23\x18\x17 \x01(\x04\x12\x1d\n\teventType\x18\x18 \x01(\x0e\x32\n.EventType\x12\r\n\x05\x65_f25\x18\x19 \x01(\x04\x12\r\n\x05\x65_f26\x18\x1a \x01(\t\x12\r\n\x05\x65_f27\x18\x1b \x01(\x04\x12\x1e\n\x16overrideMapPreferences\x18\x1c \x01(\x08\x12\"\n\x1ainvisibleToNonParticipants\x18\x1d \x01(\x08\x12&\n\x08\x65vSeries\x18\x1e \x01(\x0b\x32\x14.EventSeriesProtobuf\x12\x0c\n\x04tags\x18\x1f \x01(\t\x12\r\n\x05\x65_f32\x18 \x01(\x04\x12\x0e\n\x06\x65_wtrl\x18! \x01(\x08\x12\x19\n\x11lateJoinInMinutes\x18\" \x01(\r\x12\x11\n\tcourse_id\x18# \x01(\x04\x12#\n\x03tto\x18$ \x01(\x0b\x32\x16.EventTimeTrialOptions\x12\r\n\x05\x65_f37\x18% \x01(\t\x12\r\n\x05\x65_f38\x18& \x01(\t\x12\r\n\x05\x65_f39\x18\' \x01(\r\x12$\n\x04msed\x18( \x01(\x0b\x32\x16.MicroserviceEventData\x12\r\n\x05\x65_f41\x18) \x03(\r\" \n\x06\x45vents\x12\x16\n\x06\x65vents\x18\x01 \x03(\x0b\x32\x06.Event*V\n\x0f\x45ventVisibility\x12\x0b\n\x07\x45V_NULL\x10\x00\x12\x10\n\x0c\x45V_PUB_SHARE\x10\x01\x12\x12\n\x0e\x45V_BY_RESOURCE\x10\x02\x12\x10\n\x0c\x45V_SHAREABLE\x10\x03*\xa5\x01\n\x0b\x45ventTypeV2\x12\x16\n\x12\x45VENT_TYPE_UNKNOWN\x10\x00\x12\x15\n\x11\x45VENT_TYPE_EFONDO\x10\x01\x12\x13\n\x0f\x45VENT_TYPE_RACE\x10\x02\x12\x19\n\x15\x45VENT_TYPE_GROUP_RIDE\x10\x03\x12\x1c\n\x18\x45VENT_TYPE_GROUP_WORKOUT\x10\x04\x12\x19\n\x15\x45VENT_TYPE_TIME_TRIAL\x10\x05*d\n\tEventType\x12\x0e\n\nET_UNKNOWN\x10\x00\x12\n\n\x06\x45\x46ONDO\x10\x01\x12\x08\n\x04RACE\x10\x02\x12\x0e\n\nGROUP_RIDE\x10\x03\x12\x11\n\rGROUP_WORKOUT\x10\x04\x12\x0e\n\nTIME_TRIAL\x10\x05*o\n\x0c\x45ventCulling\x12\x15\n\x11\x43ULLING_UNDEFINED\x10\x00\x12\x15\n\x11\x43ULLING_EVERYBODY\x10\x01\x12\x16\n\x12\x43ULLING_EVENT_ONLY\x10\x02\x12\x19\n\x15\x43ULLING_SUBGROUP_ONLY\x10\x03') + +_EVENTVISIBILITY = DESCRIPTOR.enum_types_by_name['EventVisibility'] +EventVisibility = enum_type_wrapper.EnumTypeWrapper(_EVENTVISIBILITY) +_EVENTTYPEV2 = DESCRIPTOR.enum_types_by_name['EventTypeV2'] +EventTypeV2 = enum_type_wrapper.EnumTypeWrapper(_EVENTTYPEV2) +_EVENTTYPE = DESCRIPTOR.enum_types_by_name['EventType'] +EventType = enum_type_wrapper.EnumTypeWrapper(_EVENTTYPE) +_EVENTCULLING = DESCRIPTOR.enum_types_by_name['EventCulling'] +EventCulling = enum_type_wrapper.EnumTypeWrapper(_EVENTCULLING) +EV_NULL = 0 +EV_PUB_SHARE = 1 +EV_BY_RESOURCE = 2 +EV_SHAREABLE = 3 +EVENT_TYPE_UNKNOWN = 0 +EVENT_TYPE_EFONDO = 1 +EVENT_TYPE_RACE = 2 +EVENT_TYPE_GROUP_RIDE = 3 +EVENT_TYPE_GROUP_WORKOUT = 4 +EVENT_TYPE_TIME_TRIAL = 5 +ET_UNKNOWN = 0 +EFONDO = 1 +RACE = 2 +GROUP_RIDE = 3 +GROUP_WORKOUT = 4 +TIME_TRIAL = 5 +CULLING_UNDEFINED = 0 +CULLING_EVERYBODY = 1 +CULLING_EVENT_ONLY = 2 +CULLING_SUBGROUP_ONLY = 3 - -_CATEGORY = DESCRIPTOR.message_types_by_name['Category'] +_EVENTSUBGROUPPROTOBUF = DESCRIPTOR.message_types_by_name['EventSubgroupProtobuf'] +_MICROSERVICEEVENTDATA = DESCRIPTOR.message_types_by_name['MicroserviceEventData'] +_EVENTSERIESPROTOBUF = DESCRIPTOR.message_types_by_name['EventSeriesProtobuf'] +_EVENTTIMETRIALOPTIONS = DESCRIPTOR.message_types_by_name['EventTimeTrialOptions'] _EVENT = DESCRIPTOR.message_types_by_name['Event'] _EVENTS = DESCRIPTOR.message_types_by_name['Events'] -Category = _reflection.GeneratedProtocolMessageType('Category', (_message.Message,), { - 'DESCRIPTOR' : _CATEGORY, +EventSubgroupProtobuf = _reflection.GeneratedProtocolMessageType('EventSubgroupProtobuf', (_message.Message,), { + 'DESCRIPTOR' : _EVENTSUBGROUPPROTOBUF, '__module__' : 'events_pb2' - # @@protoc_insertion_point(class_scope:Category) + # @@protoc_insertion_point(class_scope:EventSubgroupProtobuf) }) -_sym_db.RegisterMessage(Category) +_sym_db.RegisterMessage(EventSubgroupProtobuf) + +MicroserviceEventData = _reflection.GeneratedProtocolMessageType('MicroserviceEventData', (_message.Message,), { + 'DESCRIPTOR' : _MICROSERVICEEVENTDATA, + '__module__' : 'events_pb2' + # @@protoc_insertion_point(class_scope:MicroserviceEventData) + }) +_sym_db.RegisterMessage(MicroserviceEventData) + +EventSeriesProtobuf = _reflection.GeneratedProtocolMessageType('EventSeriesProtobuf', (_message.Message,), { + 'DESCRIPTOR' : _EVENTSERIESPROTOBUF, + '__module__' : 'events_pb2' + # @@protoc_insertion_point(class_scope:EventSeriesProtobuf) + }) +_sym_db.RegisterMessage(EventSeriesProtobuf) + +EventTimeTrialOptions = _reflection.GeneratedProtocolMessageType('EventTimeTrialOptions', (_message.Message,), { + 'DESCRIPTOR' : _EVENTTIMETRIALOPTIONS, + '__module__' : 'events_pb2' + # @@protoc_insertion_point(class_scope:EventTimeTrialOptions) + }) +_sym_db.RegisterMessage(EventTimeTrialOptions) Event = _reflection.GeneratedProtocolMessageType('Event', (_message.Message,), { 'DESCRIPTOR' : _EVENT, @@ -45,10 +99,24 @@ _sym_db.RegisterMessage(Events) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None - _CATEGORY._serialized_start=17 - _CATEGORY._serialized_end=688 - _EVENT._serialized_start=691 - _EVENT._serialized_end=1122 - _EVENTS._serialized_start=1124 - _EVENTS._serialized_end=1156 + _EVENTVISIBILITY._serialized_start=2201 + _EVENTVISIBILITY._serialized_end=2287 + _EVENTTYPEV2._serialized_start=2290 + _EVENTTYPEV2._serialized_end=2455 + _EVENTTYPE._serialized_start=2457 + _EVENTTYPE._serialized_end=2557 + _EVENTCULLING._serialized_start=2559 + _EVENTCULLING._serialized_end=2670 + _EVENTSUBGROUPPROTOBUF._serialized_start=32 + _EVENTSUBGROUPPROTOBUF._serialized_end=1011 + _MICROSERVICEEVENTDATA._serialized_start=1013 + _MICROSERVICEEVENTDATA._serialized_end=1116 + _EVENTSERIESPROTOBUF._serialized_start=1118 + _EVENTSERIESPROTOBUF._serialized_end=1186 + _EVENTTIMETRIALOPTIONS._serialized_start=1188 + _EVENTTIMETRIALOPTIONS._serialized_end=1315 + _EVENT._serialized_start=1318 + _EVENT._serialized_end=2165 + _EVENTS._serialized_start=2167 + _EVENTS._serialized_end=2199 # @@protoc_insertion_point(module_scope) diff --git a/protobuf/goal.proto b/protobuf/goal.proto index 684b162..86d490e 100644 --- a/protobuf/goal.proto +++ b/protobuf/goal.proto @@ -1,18 +1,20 @@ syntax = "proto2"; +//TODO: uncomment and use f14, rename fields in db message Goal { optional uint64 id = 1; optional uint64 player_id = 2; - optional uint32 f3 = 3; /* status or sport? */ - optional string name = 4; - optional uint32 type = 5; /* 0=distance, 1=time */ - optional uint32 periodicity = 6; /* 0=weekly, 1=monthly */ - optional float target_distance = 7; /* in meters. set to dur for dur goals */ - optional float target_duration = 8; /* in minutes. set to dist for dist goals */ - optional float actual_distance = 9; /* in minutes. is also set for dur goals? */ - optional float actual_duration = 10; /* in meters. is also set for dist goals? */ - optional uint64 created_on = 11; /* in ms since epoch */ - optional uint64 period_end_date = 12; /* "" */ - optional uint64 f13 = 13; /* status or sport? */ + optional int64 f3 = 3; //-> enum Sport sport + optional string name = 4; // i.e. "Monthly time goal" + optional int64 type = 5; //-> enum GoalType 0=distance, 1=time + optional int64 periodicity = 6; //-> enum GoalPeriod 0=weekly, 1=monthly + optional float target_distance = 7; //in meters. set to dur for dur goals + optional float target_duration = 8; //in minutes. set to dist for dist goals + optional float actual_distance = 9; //in meters. is also set for dur goals? + optional float actual_duration = 10; //in minutes. is also set for dist goals? + optional uint64 created_on = 11; //in ms since epoch + optional uint64 period_end_date = 12; + optional uint64 f13 = 13; //-> enum GoalStatus 0=active, 1=retired + //optional string f14 = 14; // timezone? (empty) } message Goals { diff --git a/protobuf/goal_pb2.py b/protobuf/goal_pb2.py index 28daf93..80d44a8 100644 --- a/protobuf/goal_pb2.py +++ b/protobuf/goal_pb2.py @@ -14,7 +14,7 @@ _sym_db = _symbol_database.Default() -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\ngoal.proto\"\x80\x02\n\x04Goal\x12\n\n\x02id\x18\x01 \x01(\x04\x12\x11\n\tplayer_id\x18\x02 \x01(\x04\x12\n\n\x02\x66\x33\x18\x03 \x01(\r\x12\x0c\n\x04name\x18\x04 \x01(\t\x12\x0c\n\x04type\x18\x05 \x01(\r\x12\x13\n\x0bperiodicity\x18\x06 \x01(\r\x12\x17\n\x0ftarget_distance\x18\x07 \x01(\x02\x12\x17\n\x0ftarget_duration\x18\x08 \x01(\x02\x12\x17\n\x0f\x61\x63tual_distance\x18\t \x01(\x02\x12\x17\n\x0f\x61\x63tual_duration\x18\n \x01(\x02\x12\x12\n\ncreated_on\x18\x0b \x01(\x04\x12\x17\n\x0fperiod_end_date\x18\x0c \x01(\x04\x12\x0b\n\x03\x66\x31\x33\x18\r \x01(\x04\"\x1d\n\x05Goals\x12\x14\n\x05goals\x18\x01 \x03(\x0b\x32\x05.Goal') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\ngoal.proto\"\x80\x02\n\x04Goal\x12\n\n\x02id\x18\x01 \x01(\x04\x12\x11\n\tplayer_id\x18\x02 \x01(\x04\x12\n\n\x02\x66\x33\x18\x03 \x01(\x03\x12\x0c\n\x04name\x18\x04 \x01(\t\x12\x0c\n\x04type\x18\x05 \x01(\x03\x12\x13\n\x0bperiodicity\x18\x06 \x01(\x03\x12\x17\n\x0ftarget_distance\x18\x07 \x01(\x02\x12\x17\n\x0ftarget_duration\x18\x08 \x01(\x02\x12\x17\n\x0f\x61\x63tual_distance\x18\t \x01(\x02\x12\x17\n\x0f\x61\x63tual_duration\x18\n \x01(\x02\x12\x12\n\ncreated_on\x18\x0b \x01(\x04\x12\x17\n\x0fperiod_end_date\x18\x0c \x01(\x04\x12\x0b\n\x03\x66\x31\x33\x18\r \x01(\x04\"\x1d\n\x05Goals\x12\x14\n\x05goals\x18\x01 \x03(\x0b\x32\x05.Goal') diff --git a/protobuf/hash-seeds.proto b/protobuf/hash-seeds.proto index a74416f..168dd72 100644 --- a/protobuf/hash-seeds.proto +++ b/protobuf/hash-seeds.proto @@ -1,7 +1,7 @@ syntax = "proto2"; message HashSeed { - required uint64 seed1 = 1; - required uint64 seed2 = 2; + required uint32 seed1 = 1; + required uint32 seed2 = 2; required uint64 expiryDate = 3; } diff --git a/protobuf/hash_seeds_pb2.py b/protobuf/hash_seeds_pb2.py index 256b508..5d31638 100644 --- a/protobuf/hash_seeds_pb2.py +++ b/protobuf/hash_seeds_pb2.py @@ -14,7 +14,7 @@ _sym_db = _symbol_database.Default() -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x10hash-seeds.proto\"<\n\x08HashSeed\x12\r\n\x05seed1\x18\x01 \x02(\x04\x12\r\n\x05seed2\x18\x02 \x02(\x04\x12\x12\n\nexpiryDate\x18\x03 \x02(\x04\"%\n\tHashSeeds\x12\x18\n\x05seeds\x18\x01 \x03(\x0b\x32\t.HashSeed') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x10hash-seeds.proto\"<\n\x08HashSeed\x12\r\n\x05seed1\x18\x01 \x02(\r\x12\r\n\x05seed2\x18\x02 \x02(\r\x12\x12\n\nexpiryDate\x18\x03 \x02(\x04\"%\n\tHashSeeds\x12\x18\n\x05seeds\x18\x01 \x03(\x0b\x32\t.HashSeed') diff --git a/protobuf/login-response.proto b/protobuf/login-response.proto index eb52ef8..bc86bad 100644 --- a/protobuf/login-response.proto +++ b/protobuf/login-response.proto @@ -1,29 +1,8 @@ syntax = "proto2"; /* XXX: This is a first approximation of login response. Not looked into or verified. */ - -message UDPNode { - required string ip = 1; - required uint32 port = 2; -} - -message UDPNodes { - /* First server here is not a UDP node, it's the TCP telemetry server (34.218.60.145) */ - repeated UDPNode node = 1; -} - -message APIs { - optional string todaysplan_url = 1; - optional string trainingpeaks_url = 2; -} - -message ServerInfo { - required string relay_url = 1; - required APIs apis = 2; - required uint64 time = 3; - optional UDPNodes nodes = 4; -} +import "per-session-info.proto"; message LoginResponse { required string session_state = 1; - required ServerInfo info = 2; + required PerSessionInfo info = 2; } diff --git a/protobuf/login_response_pb2.py b/protobuf/login_response_pb2.py index f4f4ab3..c4dea1e 100644 --- a/protobuf/login_response_pb2.py +++ b/protobuf/login_response_pb2.py @@ -12,45 +12,14 @@ from google.protobuf import symbol_database as _symbol_database _sym_db = _symbol_database.Default() +import per_session_info_pb2 as per__session__info__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x14login-response.proto\"#\n\x07UDPNode\x12\n\n\x02ip\x18\x01 \x02(\t\x12\x0c\n\x04port\x18\x02 \x02(\r\"\"\n\x08UDPNodes\x12\x16\n\x04node\x18\x01 \x03(\x0b\x32\x08.UDPNode\"9\n\x04\x41PIs\x12\x16\n\x0etodaysplan_url\x18\x01 \x01(\t\x12\x19\n\x11trainingpeaks_url\x18\x02 \x01(\t\"\\\n\nServerInfo\x12\x11\n\trelay_url\x18\x01 \x02(\t\x12\x13\n\x04\x61pis\x18\x02 \x02(\x0b\x32\x05.APIs\x12\x0c\n\x04time\x18\x03 \x02(\x04\x12\x18\n\x05nodes\x18\x04 \x01(\x0b\x32\t.UDPNodes\"A\n\rLoginResponse\x12\x15\n\rsession_state\x18\x01 \x02(\t\x12\x19\n\x04info\x18\x02 \x02(\x0b\x32\x0b.ServerInfo') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x14login-response.proto\x1a\x16per-session-info.proto\"E\n\rLoginResponse\x12\x15\n\rsession_state\x18\x01 \x02(\t\x12\x1d\n\x04info\x18\x02 \x02(\x0b\x32\x0f.PerSessionInfo') -_UDPNODE = DESCRIPTOR.message_types_by_name['UDPNode'] -_UDPNODES = DESCRIPTOR.message_types_by_name['UDPNodes'] -_APIS = DESCRIPTOR.message_types_by_name['APIs'] -_SERVERINFO = DESCRIPTOR.message_types_by_name['ServerInfo'] _LOGINRESPONSE = DESCRIPTOR.message_types_by_name['LoginResponse'] -UDPNode = _reflection.GeneratedProtocolMessageType('UDPNode', (_message.Message,), { - 'DESCRIPTOR' : _UDPNODE, - '__module__' : 'login_response_pb2' - # @@protoc_insertion_point(class_scope:UDPNode) - }) -_sym_db.RegisterMessage(UDPNode) - -UDPNodes = _reflection.GeneratedProtocolMessageType('UDPNodes', (_message.Message,), { - 'DESCRIPTOR' : _UDPNODES, - '__module__' : 'login_response_pb2' - # @@protoc_insertion_point(class_scope:UDPNodes) - }) -_sym_db.RegisterMessage(UDPNodes) - -APIs = _reflection.GeneratedProtocolMessageType('APIs', (_message.Message,), { - 'DESCRIPTOR' : _APIS, - '__module__' : 'login_response_pb2' - # @@protoc_insertion_point(class_scope:APIs) - }) -_sym_db.RegisterMessage(APIs) - -ServerInfo = _reflection.GeneratedProtocolMessageType('ServerInfo', (_message.Message,), { - 'DESCRIPTOR' : _SERVERINFO, - '__module__' : 'login_response_pb2' - # @@protoc_insertion_point(class_scope:ServerInfo) - }) -_sym_db.RegisterMessage(ServerInfo) - LoginResponse = _reflection.GeneratedProtocolMessageType('LoginResponse', (_message.Message,), { 'DESCRIPTOR' : _LOGINRESPONSE, '__module__' : 'login_response_pb2' @@ -61,14 +30,6 @@ _sym_db.RegisterMessage(LoginResponse) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None - _UDPNODE._serialized_start=24 - _UDPNODE._serialized_end=59 - _UDPNODES._serialized_start=61 - _UDPNODES._serialized_end=95 - _APIS._serialized_start=97 - _APIS._serialized_end=154 - _SERVERINFO._serialized_start=156 - _SERVERINFO._serialized_end=248 - _LOGINRESPONSE._serialized_start=250 - _LOGINRESPONSE._serialized_end=315 + _LOGINRESPONSE._serialized_start=48 + _LOGINRESPONSE._serialized_end=117 # @@protoc_insertion_point(module_scope) diff --git a/protobuf/make.bat b/protobuf/make.bat new file mode 100644 index 0000000..5d762ab --- /dev/null +++ b/protobuf/make.bat @@ -0,0 +1,17 @@ +del *_pb2.py *_pb2.pyc +protoc --python_out=. activity.proto +protoc --python_out=. segment-result.proto +protoc --python_out=. profile.proto +protoc --python_out=. per-session-info.proto +protoc --python_out=. login-response.proto +protoc --python_out=. periodic-info.proto +protoc --python_out=. world.proto +protoc --python_out=. goal.proto +protoc --python_out=. zfiles.proto +protoc --python_out=. udp-node-msgs.proto +protoc --python_out=. tcp-node-msgs.proto +protoc --python_out=. hash-seeds.proto +protoc --python_out=. events.proto +protoc --python_out=. variants.proto + +pause \ No newline at end of file diff --git a/protobuf/per-session-info.proto b/protobuf/per-session-info.proto index 950e7ff..c323b20 100644 --- a/protobuf/per-session-info.proto +++ b/protobuf/per-session-info.proto @@ -1,4 +1,25 @@ syntax = "proto2"; +message TcpAddress { + optional string ip = 1; + optional int32 port = 2; + optional int32 lb_realm = 3; //load balancing cluster: server realm or 0 (generic) + optional int32 lb_course = 4; //load balancing cluster: course id (see also TcpAddressService::updateAddresses) +} + +message TcpConfig { + //First server: the TCP telemetry server (34.218.60.145) + repeated TcpAddress nodes = 1; +} + +message PartnersUrls { + optional string todaysplan_url = 1; + optional string trainingpeaks_url = 2; +} + message PerSessionInfo { required string relay_url = 1; + optional PartnersUrls apis = 2; + optional uint64 time = 3; + optional TcpConfig nodes = 4; + optional int32 maxSegmSubscrs = 5; //if received, sub_718DE99570 puts log message "Received max allowed segment subscriptions from session: %d", m_maxSegmSubscrs and stores it into GlobalState... } diff --git a/protobuf/per_session_info_pb2.py b/protobuf/per_session_info_pb2.py index 203ef68..7d0975b 100644 --- a/protobuf/per_session_info_pb2.py +++ b/protobuf/per_session_info_pb2.py @@ -14,11 +14,35 @@ _sym_db = _symbol_database.Default() -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16per-session-info.proto\"#\n\x0ePerSessionInfo\x12\x11\n\trelay_url\x18\x01 \x02(\t') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16per-session-info.proto\"K\n\nTcpAddress\x12\n\n\x02ip\x18\x01 \x01(\t\x12\x0c\n\x04port\x18\x02 \x01(\x05\x12\x10\n\x08lb_realm\x18\x03 \x01(\x05\x12\x11\n\tlb_course\x18\x04 \x01(\x05\"\'\n\tTcpConfig\x12\x1a\n\x05nodes\x18\x01 \x03(\x0b\x32\x0b.TcpAddress\"A\n\x0cPartnersUrls\x12\x16\n\x0etodaysplan_url\x18\x01 \x01(\t\x12\x19\n\x11trainingpeaks_url\x18\x02 \x01(\t\"\x81\x01\n\x0ePerSessionInfo\x12\x11\n\trelay_url\x18\x01 \x02(\t\x12\x1b\n\x04\x61pis\x18\x02 \x01(\x0b\x32\r.PartnersUrls\x12\x0c\n\x04time\x18\x03 \x01(\x04\x12\x19\n\x05nodes\x18\x04 \x01(\x0b\x32\n.TcpConfig\x12\x16\n\x0emaxSegmSubscrs\x18\x05 \x01(\x05') +_TCPADDRESS = DESCRIPTOR.message_types_by_name['TcpAddress'] +_TCPCONFIG = DESCRIPTOR.message_types_by_name['TcpConfig'] +_PARTNERSURLS = DESCRIPTOR.message_types_by_name['PartnersUrls'] _PERSESSIONINFO = DESCRIPTOR.message_types_by_name['PerSessionInfo'] +TcpAddress = _reflection.GeneratedProtocolMessageType('TcpAddress', (_message.Message,), { + 'DESCRIPTOR' : _TCPADDRESS, + '__module__' : 'per_session_info_pb2' + # @@protoc_insertion_point(class_scope:TcpAddress) + }) +_sym_db.RegisterMessage(TcpAddress) + +TcpConfig = _reflection.GeneratedProtocolMessageType('TcpConfig', (_message.Message,), { + 'DESCRIPTOR' : _TCPCONFIG, + '__module__' : 'per_session_info_pb2' + # @@protoc_insertion_point(class_scope:TcpConfig) + }) +_sym_db.RegisterMessage(TcpConfig) + +PartnersUrls = _reflection.GeneratedProtocolMessageType('PartnersUrls', (_message.Message,), { + 'DESCRIPTOR' : _PARTNERSURLS, + '__module__' : 'per_session_info_pb2' + # @@protoc_insertion_point(class_scope:PartnersUrls) + }) +_sym_db.RegisterMessage(PartnersUrls) + PerSessionInfo = _reflection.GeneratedProtocolMessageType('PerSessionInfo', (_message.Message,), { 'DESCRIPTOR' : _PERSESSIONINFO, '__module__' : 'per_session_info_pb2' @@ -29,6 +53,12 @@ _sym_db.RegisterMessage(PerSessionInfo) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None - _PERSESSIONINFO._serialized_start=26 - _PERSESSIONINFO._serialized_end=61 + _TCPADDRESS._serialized_start=26 + _TCPADDRESS._serialized_end=101 + _TCPCONFIG._serialized_start=103 + _TCPCONFIG._serialized_end=142 + _PARTNERSURLS._serialized_start=144 + _PARTNERSURLS._serialized_end=209 + _PERSESSIONINFO._serialized_start=212 + _PERSESSIONINFO._serialized_end=341 # @@protoc_insertion_point(module_scope) diff --git a/protobuf/periodic-info.proto b/protobuf/periodic-info.proto index 9b7b95e..f0be1e3 100644 --- a/protobuf/periodic-info.proto +++ b/protobuf/periodic-info.proto @@ -1,5 +1,6 @@ syntax = "proto2"; -message PeriodicInfo { +//TODO: check if used +message PeriodicInfo { // very similar to TcpAddress, did not found in ZwiftApp.exe yet required string game_server_ip = 1; optional uint32 f2 = 2; optional uint32 f3 = 3; @@ -8,7 +9,6 @@ message PeriodicInfo { optional uint32 f6 = 6; } - -message PeriodicInfos { +message PeriodicInfos { // very similar to TcpConfig repeated PeriodicInfo infos = 1; } diff --git a/protobuf/profile.proto b/protobuf/profile.proto index 86fd2e0..13d02b4 100644 --- a/protobuf/profile.proto +++ b/protobuf/profile.proto @@ -1,240 +1,328 @@ syntax = "proto2"; -message Profile { - optional int64 id = 1; - optional int32 is_connected_to_strava = 2; - optional string email = 3; - optional string first_name = 4; - optional string last_name = 5; - optional bool is_male = 6; - optional bytes f7 = 7; - optional uint32 weight_in_grams = 9; - optional uint32 ftp = 10; - optional uint32 f11 = 11; - optional uint32 f12 = 12; - optional uint32 f13 = 13; - optional uint32 f14 = 14; - optional uint32 f15 = 15; - optional uint32 f16 = 16; - optional uint32 f17 = 17; - optional uint32 f18 = 18; - optional uint32 f19 = 19; - optional fixed32 f20 = 20; - optional fixed32 f21 = 21; - optional fixed32 f22 = 22; - optional fixed32 f23 = 23; - optional fixed32 f24 = 24; - optional fixed32 f25 = 25; - optional fixed32 f26 = 26; - optional fixed64 f27 = 27; - optional fixed64 f28 = 28; - optional fixed64 f29 = 29; - optional fixed64 f30 = 30; - optional fixed64 f31 = 31; - optional fixed64 f32 = 32; - optional bytes f33 = 33; - optional uint32 country_code = 34; - optional uint32 total_distance_in_meters = 35; - optional uint32 elevation_gain_in_meters = 36; - optional uint32 time_ridden_in_minutes = 37; - optional uint32 f38 = 38; /* time in jersey X */ - optional uint32 f39 = 39; /* time in jersey Y */ - optional uint32 f40 = 40; /* time in jersey Z */ - optional uint32 total_watt_hours = 41; - optional uint32 height_in_millimeters = 42; - optional string dob = 43; - optional uint32 f44 = 44; - optional bool f45 = 45; - optional uint32 total_xp = 46; - optional uint32 f47 = 47; - - optional PlayerType player_type = 48; - enum PlayerType { - PLAYERTYPE0 = 0; - NORMAL = 1; - PLAYERTYPE2 = 2; - PLAYERTYPE3 = 3; - PLAYERTYPE4 = 4; - } - - optional uint32 achievement_level = 49; - optional bool f50 = 50; - optional bool f51 = 51; - optional uint32 f52 = 52; - optional uint32 f53 = 53; - optional uint32 f54 = 54; - optional uint32 age = 55; - optional fixed32 f56 = 56; - optional uint32 f57 = 57; - optional bytes f58 = 58; - optional fixed64 f59 = 59; - repeated bytes f60 = 60; /* related to subscription/billing */ - - optional ProfileSocialFacts social_facts = 61; - message ProfileSocialFacts { - optional int64 profile_id = 1; - optional int64 f2 = 2; - optional int64 f3 = 3; - optional int64 f4 = 4; - optional ProfileFollowStatus f5 = 5; - optional ProfileFollowStatus f6 = 6; - optional bool f7 = 7; - } - - optional ProfileFollowStatus f62 = 62; - optional bool f63 = 63; - optional bool f64 = 64; - - optional ProfileEnrolledProgram f65 = 65; - enum ProfileEnrolledProgram { - ENROLLEDPROGRAM0 = 0; - ENROLLEDPROGRAM1 = 1; - ENROLLEDPROGRAM2 = 2; - ENROLLEDPROGRAM3 = 3; - ENROLLEDPROGRAM4 = 4; - } - - optional bytes f66 = 66; - optional uint32 f67 = 67; - optional fixed32 f68 = 68; - optional fixed32 f69 = 69; - optional fixed32 f70 = 70; - optional fixed32 f71 = 71; - optional fixed32 f72 = 72; - optional fixed32 f73 = 73; - optional uint32 f74 = 74; - optional uint32 f75 = 75; - optional fixed32 f76 = 76; - optional fixed32 f77 = 77; - optional fixed32 f78 = 78; - optional fixed32 f79 = 79; - optional uint32 f80 = 80; - optional uint32 f81 = 81; - optional Subscription f82 = 82; - enum Sport { - SPORT0 = 0; - SPORT1 = 1; - SPORT2 = 2; - SPORT3 = 3; - SPORT4 = 4; - } - optional string mix_panel_distinct_id = 83; - optional uint32 f84 = 84; - optional uint32 f85 = 85; - optional Sport sport = 86; - optional uint32 f87 = 87; - optional bool f88 = 88; - optional string preferred_language = 89; - optional uint32 f90 = 90; - optional uint32 f91 = 91; - optional uint32 f92 = 92; - optional uint32 f93 = 93; - optional uint32 f94 = 94; - optional uint32 f95 = 95; - optional uint32 f96 = 96; - optional uint32 f97 = 97; - optional uint32 f98 = 98; - optional uint32 f99 = 99; - optional uint32 f100 = 100; - optional uint32 f101 = 101; - optional uint32 f102 = 102; - optional uint32 f103 = 103; - optional uint32 f104 = 104; - optional bool f105 = 105; - optional bool f106 = 106; - repeated bytes f107 = 107; - optional string launched_game_client = 108; - optional int64 f109 = 109; - optional bool f110 = 110; - repeated PacerSetting f114 = 114; - optional int32 f117 = 117; - optional int32 f118 = 118; - optional int32 f119 = 119; - optional int32 f120 = 120; - optional int32 f121 = 121; - optional int32 f125 = 125; +//TODO: answer ??? questions and saved_game format (zwift_profile.ksy) +enum ActivityPrivacyType { + PUBLIC = 0; + PRIVATE = 1; + FRIENDS = 2; +} +enum Sport { + CYCLING = 0; + RUNNING = 1; + ROWING = 2; + SPORT3 = 3; + SPORT4 = 4; +} +enum PlayerType { + PLAYERTYPE0 = 0; + NORMAL = 1; + PRO_CYCLIST = 2; + ZWIFT_STAFF = 3; + AMBASSADOR = 4; + VERIFIED = 5; + ZED = 6; + ZAC = 7; + PRO_TRIATHLETE = 8; + PRO_RUNNER = 9; +} +enum PowerType { + PT_VIRTUAL = 0; + PT_METER = 1; +} +message AchievementEntry { + required int32 id = 1; +} +message Achievements { + repeated AchievementEntry achievements = 1; } -message Profiles { - repeated Profile profiles = 1; +message PlayerProfile { + optional int64 id = 1; + optional int64 server_realm = 2; + optional string email = 3; + optional string first_name = 4; + optional string last_name = 5; + optional bool is_male = 6; + optional string f7 = 7; //??? empty + // no f8 exists + optional uint32 weight_in_grams = 9; + optional uint32 ftp = 10; + optional uint32 f11 = 11; //??? empty + optional uint32 body_type = 12; + optional uint32 hair_type = 13; + optional uint32 facial_hair_type = 14; + optional uint32 ride_helmet_type = 15; + optional uint32 glasses_type = 16; + optional uint32 ride_shoes_type = 17; + optional uint32 ride_socks_type = 18; + optional uint32 ride_gloves = 19; + optional fixed32 ride_jersey = 20; + optional fixed32 f21 = 21; //??? empty + optional fixed32 bike_wheel_front = 22; + optional fixed32 bike_wheel_rear = 23; + optional fixed32 bike_frame = 24; + optional fixed32 f25 = 25; //??? empty + optional fixed32 f26 = 26; //??? empty + optional fixed64 bike_frame_colour = 27; + optional fixed64 f28 = 28; //??? empty + optional fixed64 f29 = 29; //??? empty + optional fixed64 f30 = 30; //??? empty + optional fixed64 f31 = 31; //??? empty + optional fixed64 f32 = 32; //??? empty + optional bytes saved_game = 33; //for format look at zwift_profile.ksy + optional uint32 country_code = 34; + optional uint32 total_distance_in_meters = 35; + optional uint32 elevation_gain_in_meters = 36; + optional uint32 time_ridden_in_minutes = 37; + optional uint32 total_in_kom_jersey = 38; + optional uint32 total_in_sprinters_jersey = 39; + optional uint32 total_in_orange_jersey = 40; + optional uint32 total_watt_hours = 41; // = calories * 0.3256381927080305 + optional uint32 height_in_millimeters = 42; + optional string dob = 43; + optional uint32 max_heart_rate = 44; + optional bool connected_to_strava = 45; + optional uint32 total_xp = 46; + optional uint32 total_gold_drops = 47; + optional PlayerType player_type = 48; + optional uint32 achievement_level = 49; + optional bool use_metric = 50; + optional bool strava_premium = 51; + optional PowerType power_source_model = 52; + optional uint32 f53 = 53; //??? empty + optional uint32 f54 = 54; //??? empty + optional uint32 age = 55; + optional fixed32 f56 = 56; //??? empty + optional uint32 f57 = 57; //??? empty + optional string large_avatar_url = 58; + optional fixed64 privacy_bits = 59; + repeated ProfileEntitlement entitlements = 60; + + optional SocialFacts social_facts = 61; + message SocialFacts { + optional int64 profile_id = 1; + optional int32 followers_count = 2; + optional int32 followees_count = 3; + optional int32 followees_in_common_with_logged_in_player = 4; + optional FollowStatus follower_status_of_logged_in_player = 5; + optional FollowStatus followee_status_of_logged_in_player = 6; + optional bool is_favorite_of_logged_in_player = 7; + } + + optional FollowStatus follow_status = 62; + optional bool connected_to_training_peaks = 63; + optional bool connected_to_todays_plan = 64; + + optional EnrolledProgram enrolled_program = 65; + enum EnrolledProgram { + ENROLLEDPROGRAM0 = 0; + ZWIFT_ACADEMY = 1; + ENROLLEDPROGRAM2 = 2; + ENROLLEDPROGRAM3 = 3; + ENROLLEDPROGRAM4 = 4; + } + + optional string todayplan_url = 66; + optional uint32 f67 = 67; //??? empty + optional fixed32 run_shirt_type = 68; + optional fixed32 run_shorts_type = 69; + optional fixed32 run_shoes_type = 70; + optional fixed32 run_socks_type = 71; + optional fixed32 run_helmet_type = 72; + optional fixed32 run_arm_accessory = 73; + optional uint32 total_run_distance = 74; + optional uint32 total_run_experience_points = 75; + optional fixed32 f76 = 76; //??? empty + optional fixed32 f77 = 77; //??? empty + optional fixed32 f78 = 78; //??? empty + optional fixed32 f79 = 79; //??? empty + optional uint32 f80 = 80; //??? empty + optional uint32 f81 = 81; //??? empty + optional Subscription subscription = 82; + optional string mix_panel_distinct_id = 83; + optional uint32 run_achievement_level = 84; + optional uint32 total_run_time_in_minutes = 85; + optional Sport sport = 86; + optional uint32 utc_offset_in_minutes = 87; + optional bool connected_to_under_armour = 88; + optional string preferred_language = 89; + optional uint32 hair_colour = 90; + optional uint32 facial_hair_colour = 91; + optional uint32 f92 = 92; //??? empty + optional uint32 f93 = 93; //??? empty + optional uint32 run_shorts_length = 94; + optional uint32 f95 = 95; //??? empty + optional uint32 run_socks_length = 96; + optional uint32 f97 = 97; //??? empty + optional uint32 ride_socks_length = 98; + optional uint32 f99 = 99; //??? empty + optional uint32 f100 = 100; //??? empty + optional uint32 f101 = 101; //??? empty + optional uint32 f102 = 102; //??? empty + optional uint32 f103 = 103; //??? empty + optional uint32 f104 = 104; //??? empty + optional bool connected_to_withings = 105; + optional bool connected_to_fitbit = 106; + // no 107 repeated bytes + optional string launched_game_client = 108; + optional int64 current_activity_id = 109; + optional bool connected_to_garmin = 110; + message Reminder { + optional int64 f1 = 1; + optional string f2 = 2; + optional int64 f3 = 3; + message ReminderProperty { + optional int64 f1 = 1; + optional string f2 = 2; + optional string f3 = 3; + } + repeated ReminderProperty f4 = 4; + } + repeated Reminder reminders = 111; + optional bool f112 = 112; //??? empty + repeated Attribute private_attributes = 113; + repeated Attribute public_attributes = 114; + optional int32 total_run_calories = 115; + optional int64 f116 = 116; //??? empty + optional int32 run_time_1mi_in_seconds = 117; + optional int32 run_time_5km_in_seconds = 118; + optional int32 run_time_10km_in_seconds = 119; + optional int32 run_time_half_marathon_in_seconds = 120; + optional int32 run_time_full_marathon_in_seconds = 121; + optional int32 f122 = 122; //??? empty + enum CyclingOrganization { + NO_CYCLING_LICENSE = 0; + CYCLING_SOUTH_AFRICA = 1; + CYCLING_AUSTRALIA = 2; + CYCLING_NEW_ZEALAND = 3; + } + optional CyclingOrganization cycling_organization = 123; + optional string f124 = 124; // LICENSE_NUMBER/E_NUMBER + optional ActivityPrivacyType default_activity_privacy = 125; + optional bool connected_to_runtastic = 126; + repeated PropertyChange property_changes = 127; +} + +message PlayerProfiles { + repeated PlayerProfile profiles = 1; } message ProfileEntitlement { - optional EntitlementType f1 = 1; - enum EntitlementType { - ENTITLEMENTTYPE0 = 0; - ENTITLEMENTTYPE1 = 1; - ENTITLEMENTTYPE2 = 2; - ENTITLEMENTTYPE3 = 3; - ENTITLEMENTTYPE4 = 4; - } - - optional int64 f2 = 2; - - optional ProfileEntitlementStatus c = 3; - enum ProfileEntitlementStatus { - ENTITLEMENTSTATUS0 = 0; - ENTITLEMENTSTATUS1 = 1; - ENTITLEMENTSTATUS2 = 2; - ENTITLEMENTSTATUS3 = 3; - ENTITLEMENTSTATUS4 = 4; - } - - optional bytes f4 = 4; - optional uint32 f5 = 5; - optional uint32 f6 = 6; - optional uint32 f7 = 7; - optional uint32 f8 = 8; - optional uint32 f9 = 9; - optional bytes f10 = 10; - - optional Platform f11 = 11; - enum Platform { - PLATFORM0 = 0; - PLATFORM1 = 1; - PLATFORM2 = 2; - PLATFORM3 = 3; - PLATFORM4 = 4; - PLATFORM5 = 5; - PLATFORM6 = 6; - } - - optional uint32 f12 = 12; - optional bool f13 = 13; + optional EntitlementType type = 1; + enum EntitlementType { + ENTITLEMENTTYPE0 = 0; + RIDE = 1; + RUN = 2; + ROW = 3; + USE = 4; + } + + optional int64 f2 = 2; // always -1: legacy? + + optional ProfileEntitlementStatus status = 3; + enum ProfileEntitlementStatus { + ENTITLEMENTSTATUS0 = 0; + EXPIRED = 1; + ACTIVE = 2; + CANCELED = 3; + INACTIVE = 4; + APPLIED_AS_SUBSCRIPTION_TRIAL_PERIOD = 5; + } + + optional string period = 4; // 'P7D' = period of 7 days (Y, M also supported for year and month) + optional uint32 begin_time_unix = 5; //when period started + optional uint32 end_time_unix = 6; //when period ended + optional uint32 kilometers = 7; //25 every month + optional uint32 begin_total_distance = 8; //where every-month gift started + optional uint32 end_total_distance = 9; //where every-month gift should end + optional string source = 10; // for example, "strava.premium" ? + + optional Platform platform = 11; // legacy? + enum Platform { + PLATFORM_OSX = 0; + PLATFORM_PC = 1; + PLATFORM_IOS = 2; + PLATFORM_ANDROID = 3; + PLATFORM_TVOS = 4; + PLATFORM5 = 5; + PLATFORM6 = 6; + } + + optional uint32 renewal_date_unix = 12; //when next 25km gift renewed + optional bool new_trial_system = 13; //do not interrupt current track if trial ended + repeated Platform platforms = 14; } -enum ProfileFollowStatus { - FOLLOWSTATUS0 = 0; - SELF = 1; - FOLLOWSTATUS2 = 2; - FOLLOWSTATUS3 = 3; - FOLLOWSTATUS4 = 4; +enum FollowStatus { + FOLLOWSTATUS0 = 0; + UNKNOWN = 1; + REQUESTS_TO_FOLLOW = 2; + IS_FOLLOWING = 3; + HAS_BEEN_DECLINED = 7; + IS_BLOCKED = 4; + NO_RELATIONSHIP = 5; + SELF = 6; } message Subscription { - optional Gateway f1 = 1; - enum Gateway { - GATEWAY0 = 0; - GATEWAY1 = 1; - GATEWAY2 = 2; - GATEWAY3 = 3; - GATEWAY4 = 4; - } - - optional SubscriptionStatus f2 = 2; - enum SubscriptionStatus { - STATUS0 = 0; - STATUS1 = 1; - ACTIVE = 2; - ACTIVE_CANCELLED = 3; - STATUS4 = 4; - STATUS5 = 5; - STATUS6 = 6; - } + optional Gateway gateway = 1; + enum Gateway { + BRAINTREE = 0; + APPLE = 1; + } + + optional SubscriptionStatus status = 2; + enum SubscriptionStatus { + NEW = 0; + EXPIRED = 1; + ACTIVE = 2; + CANCELED = 3; + PAST_DUE = 4; + PENDING = 5; + SUBERROR = 6; + UNRECOGNIZED = 7; + UNKNOWN = 8; + ACTIVE_WITH_PAYMENT_FAILURE = 9; + ABANDONED = 10; + } } -message PacerSetting { - required int64 id = 1; - optional uint64 number_value = 2; - optional fixed32 hex_value = 3; - optional string string_value = 5; +message PropertyChange { + enum Id { + TYPE0 = 0; + DATE_OF_BIRTH = 1; + GENDER = 2; + } + required Id property_name = 1; + optional int32 change_count = 2; + optional int32 max_changes = 3; +} +/* +Attribute ID is crc32 of it's name. Examples: +public +324889996=0x135D6D8C (0) +private +-1575272099="TODAYS_SPORT_SELECT_TYPE" (CYCLIST) +1169650385="PLAYER_CACHE_BLOB" (000000) +1025311738="ONBOARD_CUSTOMIZATION_USER_TYPE_ZWIFT_GOAL" (GET IN SHAPE/TRAINING) +-1482469514="LEVEL50" (1) +839250175="TRAINING_PLAN_DETAILS" ("") +1190707182="LAST_RATING_TIME" (1641574907) +-702503934="XPTODROPSCONVERSIONDONE" (1) +2076353160="USERRIDECOUNT" (4) +-2012319163="LAST_WORKOUT_HASH" (1007947233) +-1001004453="SPORT_SELECT_TYPE" (CYCLIST) +1857228933="USAGE_HISTOGRAM_1" (0,...) +2004261226="DROPS_CURRENTSESSION" (0.0) +1318665884="USERPROFILE_STEERINGTUTORIALSHOWN" (1) +568968402="MOUNTAIN_TRAIL_STARTED" (7) +-642877525="MOUNTAIN_TRAIL_COMPLETED" (5) +-1414690690="USERPROFILE_STEERINGPAIRED" (1) +-34579778="ONROAD_SURVEY_COMPLETED" (5) +-1316403440="PACERBOTTUTORIAL" (1) +*/ +message Attribute { + required int32 id = 1; + optional int64 number_value = 2; + optional float float_value = 3; + optional string string_value = 5; } diff --git a/protobuf/profile_pb2.py b/protobuf/profile_pb2.py index 6dbdef8..f5154d3 100644 --- a/protobuf/profile_pb2.py +++ b/protobuf/profile_pb2.py @@ -15,52 +15,118 @@ _sym_db = _symbol_database.Default() -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\rprofile.proto\"\xcd\x12\n\x07Profile\x12\n\n\x02id\x18\x01 \x01(\x03\x12\x1e\n\x16is_connected_to_strava\x18\x02 \x01(\x05\x12\r\n\x05\x65mail\x18\x03 \x01(\t\x12\x12\n\nfirst_name\x18\x04 \x01(\t\x12\x11\n\tlast_name\x18\x05 \x01(\t\x12\x0f\n\x07is_male\x18\x06 \x01(\x08\x12\n\n\x02\x66\x37\x18\x07 \x01(\x0c\x12\x17\n\x0fweight_in_grams\x18\t \x01(\r\x12\x0b\n\x03\x66tp\x18\n \x01(\r\x12\x0b\n\x03\x66\x31\x31\x18\x0b \x01(\r\x12\x0b\n\x03\x66\x31\x32\x18\x0c \x01(\r\x12\x0b\n\x03\x66\x31\x33\x18\r \x01(\r\x12\x0b\n\x03\x66\x31\x34\x18\x0e \x01(\r\x12\x0b\n\x03\x66\x31\x35\x18\x0f \x01(\r\x12\x0b\n\x03\x66\x31\x36\x18\x10 \x01(\r\x12\x0b\n\x03\x66\x31\x37\x18\x11 \x01(\r\x12\x0b\n\x03\x66\x31\x38\x18\x12 \x01(\r\x12\x0b\n\x03\x66\x31\x39\x18\x13 \x01(\r\x12\x0b\n\x03\x66\x32\x30\x18\x14 \x01(\x07\x12\x0b\n\x03\x66\x32\x31\x18\x15 \x01(\x07\x12\x0b\n\x03\x66\x32\x32\x18\x16 \x01(\x07\x12\x0b\n\x03\x66\x32\x33\x18\x17 \x01(\x07\x12\x0b\n\x03\x66\x32\x34\x18\x18 \x01(\x07\x12\x0b\n\x03\x66\x32\x35\x18\x19 \x01(\x07\x12\x0b\n\x03\x66\x32\x36\x18\x1a \x01(\x07\x12\x0b\n\x03\x66\x32\x37\x18\x1b \x01(\x06\x12\x0b\n\x03\x66\x32\x38\x18\x1c \x01(\x06\x12\x0b\n\x03\x66\x32\x39\x18\x1d \x01(\x06\x12\x0b\n\x03\x66\x33\x30\x18\x1e \x01(\x06\x12\x0b\n\x03\x66\x33\x31\x18\x1f \x01(\x06\x12\x0b\n\x03\x66\x33\x32\x18 \x01(\x06\x12\x0b\n\x03\x66\x33\x33\x18! \x01(\x0c\x12\x14\n\x0c\x63ountry_code\x18\" \x01(\r\x12 \n\x18total_distance_in_meters\x18# \x01(\r\x12 \n\x18\x65levation_gain_in_meters\x18$ \x01(\r\x12\x1e\n\x16time_ridden_in_minutes\x18% \x01(\r\x12\x0b\n\x03\x66\x33\x38\x18& \x01(\r\x12\x0b\n\x03\x66\x33\x39\x18\' \x01(\r\x12\x0b\n\x03\x66\x34\x30\x18( \x01(\r\x12\x18\n\x10total_watt_hours\x18) \x01(\r\x12\x1d\n\x15height_in_millimeters\x18* \x01(\r\x12\x0b\n\x03\x64ob\x18+ \x01(\t\x12\x0b\n\x03\x66\x34\x34\x18, \x01(\r\x12\x0b\n\x03\x66\x34\x35\x18- \x01(\x08\x12\x10\n\x08total_xp\x18. \x01(\r\x12\x0b\n\x03\x66\x34\x37\x18/ \x01(\r\x12(\n\x0bplayer_type\x18\x30 \x01(\x0e\x32\x13.Profile.PlayerType\x12\x19\n\x11\x61\x63hievement_level\x18\x31 \x01(\r\x12\x0b\n\x03\x66\x35\x30\x18\x32 \x01(\x08\x12\x0b\n\x03\x66\x35\x31\x18\x33 \x01(\x08\x12\x0b\n\x03\x66\x35\x32\x18\x34 \x01(\r\x12\x0b\n\x03\x66\x35\x33\x18\x35 \x01(\r\x12\x0b\n\x03\x66\x35\x34\x18\x36 \x01(\r\x12\x0b\n\x03\x61ge\x18\x37 \x01(\r\x12\x0b\n\x03\x66\x35\x36\x18\x38 \x01(\x07\x12\x0b\n\x03\x66\x35\x37\x18\x39 \x01(\r\x12\x0b\n\x03\x66\x35\x38\x18: \x01(\x0c\x12\x0b\n\x03\x66\x35\x39\x18; \x01(\x06\x12\x0b\n\x03\x66\x36\x30\x18< \x03(\x0c\x12\x31\n\x0csocial_facts\x18= \x01(\x0b\x32\x1b.Profile.ProfileSocialFacts\x12!\n\x03\x66\x36\x32\x18> \x01(\x0e\x32\x14.ProfileFollowStatus\x12\x0b\n\x03\x66\x36\x33\x18? \x01(\x08\x12\x0b\n\x03\x66\x36\x34\x18@ \x01(\x08\x12,\n\x03\x66\x36\x35\x18\x41 \x01(\x0e\x32\x1f.Profile.ProfileEnrolledProgram\x12\x0b\n\x03\x66\x36\x36\x18\x42 \x01(\x0c\x12\x0b\n\x03\x66\x36\x37\x18\x43 \x01(\r\x12\x0b\n\x03\x66\x36\x38\x18\x44 \x01(\x07\x12\x0b\n\x03\x66\x36\x39\x18\x45 \x01(\x07\x12\x0b\n\x03\x66\x37\x30\x18\x46 \x01(\x07\x12\x0b\n\x03\x66\x37\x31\x18G \x01(\x07\x12\x0b\n\x03\x66\x37\x32\x18H \x01(\x07\x12\x0b\n\x03\x66\x37\x33\x18I \x01(\x07\x12\x0b\n\x03\x66\x37\x34\x18J \x01(\r\x12\x0b\n\x03\x66\x37\x35\x18K \x01(\r\x12\x0b\n\x03\x66\x37\x36\x18L \x01(\x07\x12\x0b\n\x03\x66\x37\x37\x18M \x01(\x07\x12\x0b\n\x03\x66\x37\x38\x18N \x01(\x07\x12\x0b\n\x03\x66\x37\x39\x18O \x01(\x07\x12\x0b\n\x03\x66\x38\x30\x18P \x01(\r\x12\x0b\n\x03\x66\x38\x31\x18Q \x01(\r\x12\x1a\n\x03\x66\x38\x32\x18R \x01(\x0b\x32\r.Subscription\x12\x1d\n\x15mix_panel_distinct_id\x18S \x01(\t\x12\x0b\n\x03\x66\x38\x34\x18T \x01(\r\x12\x0b\n\x03\x66\x38\x35\x18U \x01(\r\x12\x1d\n\x05sport\x18V \x01(\x0e\x32\x0e.Profile.Sport\x12\x0b\n\x03\x66\x38\x37\x18W \x01(\r\x12\x0b\n\x03\x66\x38\x38\x18X \x01(\x08\x12\x1a\n\x12preferred_language\x18Y \x01(\t\x12\x0b\n\x03\x66\x39\x30\x18Z \x01(\r\x12\x0b\n\x03\x66\x39\x31\x18[ \x01(\r\x12\x0b\n\x03\x66\x39\x32\x18\\ \x01(\r\x12\x0b\n\x03\x66\x39\x33\x18] \x01(\r\x12\x0b\n\x03\x66\x39\x34\x18^ \x01(\r\x12\x0b\n\x03\x66\x39\x35\x18_ \x01(\r\x12\x0b\n\x03\x66\x39\x36\x18` \x01(\r\x12\x0b\n\x03\x66\x39\x37\x18\x61 \x01(\r\x12\x0b\n\x03\x66\x39\x38\x18\x62 \x01(\r\x12\x0b\n\x03\x66\x39\x39\x18\x63 \x01(\r\x12\x0c\n\x04\x66\x31\x30\x30\x18\x64 \x01(\r\x12\x0c\n\x04\x66\x31\x30\x31\x18\x65 \x01(\r\x12\x0c\n\x04\x66\x31\x30\x32\x18\x66 \x01(\r\x12\x0c\n\x04\x66\x31\x30\x33\x18g \x01(\r\x12\x0c\n\x04\x66\x31\x30\x34\x18h \x01(\r\x12\x0c\n\x04\x66\x31\x30\x35\x18i \x01(\x08\x12\x0c\n\x04\x66\x31\x30\x36\x18j \x01(\x08\x12\x0c\n\x04\x66\x31\x30\x37\x18k \x03(\x0c\x12\x1c\n\x14launched_game_client\x18l \x01(\t\x12\x0c\n\x04\x66\x31\x30\x39\x18m \x01(\x03\x12\x0c\n\x04\x66\x31\x31\x30\x18n \x01(\x08\x12\x1b\n\x04\x66\x31\x31\x34\x18r \x03(\x0b\x32\r.PacerSetting\x12\x0c\n\x04\x66\x31\x31\x37\x18u \x01(\x05\x12\x0c\n\x04\x66\x31\x31\x38\x18v \x01(\x05\x12\x0c\n\x04\x66\x31\x31\x39\x18w \x01(\x05\x12\x0c\n\x04\x66\x31\x32\x30\x18x \x01(\x05\x12\x0c\n\x04\x66\x31\x32\x31\x18y \x01(\x05\x12\x0c\n\x04\x66\x31\x32\x35\x18} \x01(\x05\x1a\x9c\x01\n\x12ProfileSocialFacts\x12\x12\n\nprofile_id\x18\x01 \x01(\x03\x12\n\n\x02\x66\x32\x18\x02 \x01(\x03\x12\n\n\x02\x66\x33\x18\x03 \x01(\x03\x12\n\n\x02\x66\x34\x18\x04 \x01(\x03\x12 \n\x02\x66\x35\x18\x05 \x01(\x0e\x32\x14.ProfileFollowStatus\x12 \n\x02\x66\x36\x18\x06 \x01(\x0e\x32\x14.ProfileFollowStatus\x12\n\n\x02\x66\x37\x18\x07 \x01(\x08\"\\\n\nPlayerType\x12\x0f\n\x0bPLAYERTYPE0\x10\x00\x12\n\n\x06NORMAL\x10\x01\x12\x0f\n\x0bPLAYERTYPE2\x10\x02\x12\x0f\n\x0bPLAYERTYPE3\x10\x03\x12\x0f\n\x0bPLAYERTYPE4\x10\x04\"\x86\x01\n\x16ProfileEnrolledProgram\x12\x14\n\x10\x45NROLLEDPROGRAM0\x10\x00\x12\x14\n\x10\x45NROLLEDPROGRAM1\x10\x01\x12\x14\n\x10\x45NROLLEDPROGRAM2\x10\x02\x12\x14\n\x10\x45NROLLEDPROGRAM3\x10\x03\x12\x14\n\x10\x45NROLLEDPROGRAM4\x10\x04\"C\n\x05Sport\x12\n\n\x06SPORT0\x10\x00\x12\n\n\x06SPORT1\x10\x01\x12\n\n\x06SPORT2\x10\x02\x12\n\n\x06SPORT3\x10\x03\x12\n\n\x06SPORT4\x10\x04\"&\n\x08Profiles\x12\x1a\n\x08profiles\x18\x01 \x03(\x0b\x32\x08.Profile\"\xaf\x05\n\x12ProfileEntitlement\x12/\n\x02\x66\x31\x18\x01 \x01(\x0e\x32#.ProfileEntitlement.EntitlementType\x12\n\n\x02\x66\x32\x18\x02 \x01(\x03\x12\x37\n\x01\x63\x18\x03 \x01(\x0e\x32,.ProfileEntitlement.ProfileEntitlementStatus\x12\n\n\x02\x66\x34\x18\x04 \x01(\x0c\x12\n\n\x02\x66\x35\x18\x05 \x01(\r\x12\n\n\x02\x66\x36\x18\x06 \x01(\r\x12\n\n\x02\x66\x37\x18\x07 \x01(\r\x12\n\n\x02\x66\x38\x18\x08 \x01(\r\x12\n\n\x02\x66\x39\x18\t \x01(\r\x12\x0b\n\x03\x66\x31\x30\x18\n \x01(\x0c\x12)\n\x03\x66\x31\x31\x18\x0b \x01(\x0e\x32\x1c.ProfileEntitlement.Platform\x12\x0b\n\x03\x66\x31\x32\x18\x0c \x01(\r\x12\x0b\n\x03\x66\x31\x33\x18\r \x01(\x08\"\x7f\n\x0f\x45ntitlementType\x12\x14\n\x10\x45NTITLEMENTTYPE0\x10\x00\x12\x14\n\x10\x45NTITLEMENTTYPE1\x10\x01\x12\x14\n\x10\x45NTITLEMENTTYPE2\x10\x02\x12\x14\n\x10\x45NTITLEMENTTYPE3\x10\x03\x12\x14\n\x10\x45NTITLEMENTTYPE4\x10\x04\"\x92\x01\n\x18ProfileEntitlementStatus\x12\x16\n\x12\x45NTITLEMENTSTATUS0\x10\x00\x12\x16\n\x12\x45NTITLEMENTSTATUS1\x10\x01\x12\x16\n\x12\x45NTITLEMENTSTATUS2\x10\x02\x12\x16\n\x12\x45NTITLEMENTSTATUS3\x10\x03\x12\x16\n\x12\x45NTITLEMENTSTATUS4\x10\x04\"s\n\x08Platform\x12\r\n\tPLATFORM0\x10\x00\x12\r\n\tPLATFORM1\x10\x01\x12\r\n\tPLATFORM2\x10\x02\x12\r\n\tPLATFORM3\x10\x03\x12\r\n\tPLATFORM4\x10\x04\x12\r\n\tPLATFORM5\x10\x05\x12\r\n\tPLATFORM6\x10\x06\"\xa9\x02\n\x0cSubscription\x12!\n\x02\x66\x31\x18\x01 \x01(\x0e\x32\x15.Subscription.Gateway\x12,\n\x02\x66\x32\x18\x02 \x01(\x0e\x32 .Subscription.SubscriptionStatus\"O\n\x07Gateway\x12\x0c\n\x08GATEWAY0\x10\x00\x12\x0c\n\x08GATEWAY1\x10\x01\x12\x0c\n\x08GATEWAY2\x10\x02\x12\x0c\n\x08GATEWAY3\x10\x03\x12\x0c\n\x08GATEWAY4\x10\x04\"w\n\x12SubscriptionStatus\x12\x0b\n\x07STATUS0\x10\x00\x12\x0b\n\x07STATUS1\x10\x01\x12\n\n\x06\x41\x43TIVE\x10\x02\x12\x14\n\x10\x41\x43TIVE_CANCELLED\x10\x03\x12\x0b\n\x07STATUS4\x10\x04\x12\x0b\n\x07STATUS5\x10\x05\x12\x0b\n\x07STATUS6\x10\x06\"Y\n\x0cPacerSetting\x12\n\n\x02id\x18\x01 \x02(\x03\x12\x14\n\x0cnumber_value\x18\x02 \x01(\x04\x12\x11\n\thex_value\x18\x03 \x01(\x07\x12\x14\n\x0cstring_value\x18\x05 \x01(\t*k\n\x13ProfileFollowStatus\x12\x11\n\rFOLLOWSTATUS0\x10\x00\x12\x08\n\x04SELF\x10\x01\x12\x11\n\rFOLLOWSTATUS2\x10\x02\x12\x11\n\rFOLLOWSTATUS3\x10\x03\x12\x11\n\rFOLLOWSTATUS4\x10\x04') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\rprofile.proto\"\x1e\n\x10\x41\x63hievementEntry\x12\n\n\x02id\x18\x01 \x02(\x05\"7\n\x0c\x41\x63hievements\x12\'\n\x0c\x61\x63hievements\x18\x01 \x03(\x0b\x32\x11.AchievementEntry\"\xd5\x1d\n\rPlayerProfile\x12\n\n\x02id\x18\x01 \x01(\x03\x12\x14\n\x0cserver_realm\x18\x02 \x01(\x03\x12\r\n\x05\x65mail\x18\x03 \x01(\t\x12\x12\n\nfirst_name\x18\x04 \x01(\t\x12\x11\n\tlast_name\x18\x05 \x01(\t\x12\x0f\n\x07is_male\x18\x06 \x01(\x08\x12\n\n\x02\x66\x37\x18\x07 \x01(\t\x12\x17\n\x0fweight_in_grams\x18\t \x01(\r\x12\x0b\n\x03\x66tp\x18\n \x01(\r\x12\x0b\n\x03\x66\x31\x31\x18\x0b \x01(\r\x12\x11\n\tbody_type\x18\x0c \x01(\r\x12\x11\n\thair_type\x18\r \x01(\r\x12\x18\n\x10\x66\x61\x63ial_hair_type\x18\x0e \x01(\r\x12\x18\n\x10ride_helmet_type\x18\x0f \x01(\r\x12\x14\n\x0cglasses_type\x18\x10 \x01(\r\x12\x17\n\x0fride_shoes_type\x18\x11 \x01(\r\x12\x17\n\x0fride_socks_type\x18\x12 \x01(\r\x12\x13\n\x0bride_gloves\x18\x13 \x01(\r\x12\x13\n\x0bride_jersey\x18\x14 \x01(\x07\x12\x0b\n\x03\x66\x32\x31\x18\x15 \x01(\x07\x12\x18\n\x10\x62ike_wheel_front\x18\x16 \x01(\x07\x12\x17\n\x0f\x62ike_wheel_rear\x18\x17 \x01(\x07\x12\x12\n\nbike_frame\x18\x18 \x01(\x07\x12\x0b\n\x03\x66\x32\x35\x18\x19 \x01(\x07\x12\x0b\n\x03\x66\x32\x36\x18\x1a \x01(\x07\x12\x19\n\x11\x62ike_frame_colour\x18\x1b \x01(\x06\x12\x0b\n\x03\x66\x32\x38\x18\x1c \x01(\x06\x12\x0b\n\x03\x66\x32\x39\x18\x1d \x01(\x06\x12\x0b\n\x03\x66\x33\x30\x18\x1e \x01(\x06\x12\x0b\n\x03\x66\x33\x31\x18\x1f \x01(\x06\x12\x0b\n\x03\x66\x33\x32\x18 \x01(\x06\x12\x12\n\nsaved_game\x18! \x01(\x0c\x12\x14\n\x0c\x63ountry_code\x18\" \x01(\r\x12 \n\x18total_distance_in_meters\x18# \x01(\r\x12 \n\x18\x65levation_gain_in_meters\x18$ \x01(\r\x12\x1e\n\x16time_ridden_in_minutes\x18% \x01(\r\x12\x1b\n\x13total_in_kom_jersey\x18& \x01(\r\x12!\n\x19total_in_sprinters_jersey\x18\' \x01(\r\x12\x1e\n\x16total_in_orange_jersey\x18( \x01(\r\x12\x18\n\x10total_watt_hours\x18) \x01(\r\x12\x1d\n\x15height_in_millimeters\x18* \x01(\r\x12\x0b\n\x03\x64ob\x18+ \x01(\t\x12\x16\n\x0emax_heart_rate\x18, \x01(\r\x12\x1b\n\x13\x63onnected_to_strava\x18- \x01(\x08\x12\x10\n\x08total_xp\x18. \x01(\r\x12\x18\n\x10total_gold_drops\x18/ \x01(\r\x12 \n\x0bplayer_type\x18\x30 \x01(\x0e\x32\x0b.PlayerType\x12\x19\n\x11\x61\x63hievement_level\x18\x31 \x01(\r\x12\x12\n\nuse_metric\x18\x32 \x01(\x08\x12\x16\n\x0estrava_premium\x18\x33 \x01(\x08\x12&\n\x12power_source_model\x18\x34 \x01(\x0e\x32\n.PowerType\x12\x0b\n\x03\x66\x35\x33\x18\x35 \x01(\r\x12\x0b\n\x03\x66\x35\x34\x18\x36 \x01(\r\x12\x0b\n\x03\x61ge\x18\x37 \x01(\r\x12\x0b\n\x03\x66\x35\x36\x18\x38 \x01(\x07\x12\x0b\n\x03\x66\x35\x37\x18\x39 \x01(\r\x12\x18\n\x10large_avatar_url\x18: \x01(\t\x12\x14\n\x0cprivacy_bits\x18; \x01(\x06\x12)\n\x0c\x65ntitlements\x18< \x03(\x0b\x32\x13.ProfileEntitlement\x12\x30\n\x0csocial_facts\x18= \x01(\x0b\x32\x1a.PlayerProfile.SocialFacts\x12$\n\rfollow_status\x18> \x01(\x0e\x32\r.FollowStatus\x12#\n\x1b\x63onnected_to_training_peaks\x18? \x01(\x08\x12 \n\x18\x63onnected_to_todays_plan\x18@ \x01(\x08\x12\x38\n\x10\x65nrolled_program\x18\x41 \x01(\x0e\x32\x1e.PlayerProfile.EnrolledProgram\x12\x15\n\rtodayplan_url\x18\x42 \x01(\t\x12\x0b\n\x03\x66\x36\x37\x18\x43 \x01(\r\x12\x16\n\x0erun_shirt_type\x18\x44 \x01(\x07\x12\x17\n\x0frun_shorts_type\x18\x45 \x01(\x07\x12\x16\n\x0erun_shoes_type\x18\x46 \x01(\x07\x12\x16\n\x0erun_socks_type\x18G \x01(\x07\x12\x17\n\x0frun_helmet_type\x18H \x01(\x07\x12\x19\n\x11run_arm_accessory\x18I \x01(\x07\x12\x1a\n\x12total_run_distance\x18J \x01(\r\x12#\n\x1btotal_run_experience_points\x18K \x01(\r\x12\x0b\n\x03\x66\x37\x36\x18L \x01(\x07\x12\x0b\n\x03\x66\x37\x37\x18M \x01(\x07\x12\x0b\n\x03\x66\x37\x38\x18N \x01(\x07\x12\x0b\n\x03\x66\x37\x39\x18O \x01(\x07\x12\x0b\n\x03\x66\x38\x30\x18P \x01(\r\x12\x0b\n\x03\x66\x38\x31\x18Q \x01(\r\x12#\n\x0csubscription\x18R \x01(\x0b\x32\r.Subscription\x12\x1d\n\x15mix_panel_distinct_id\x18S \x01(\t\x12\x1d\n\x15run_achievement_level\x18T \x01(\r\x12!\n\x19total_run_time_in_minutes\x18U \x01(\r\x12\x15\n\x05sport\x18V \x01(\x0e\x32\x06.Sport\x12\x1d\n\x15utc_offset_in_minutes\x18W \x01(\r\x12!\n\x19\x63onnected_to_under_armour\x18X \x01(\x08\x12\x1a\n\x12preferred_language\x18Y \x01(\t\x12\x13\n\x0bhair_colour\x18Z \x01(\r\x12\x1a\n\x12\x66\x61\x63ial_hair_colour\x18[ \x01(\r\x12\x0b\n\x03\x66\x39\x32\x18\\ \x01(\r\x12\x0b\n\x03\x66\x39\x33\x18] \x01(\r\x12\x19\n\x11run_shorts_length\x18^ \x01(\r\x12\x0b\n\x03\x66\x39\x35\x18_ \x01(\r\x12\x18\n\x10run_socks_length\x18` \x01(\r\x12\x0b\n\x03\x66\x39\x37\x18\x61 \x01(\r\x12\x19\n\x11ride_socks_length\x18\x62 \x01(\r\x12\x0b\n\x03\x66\x39\x39\x18\x63 \x01(\r\x12\x0c\n\x04\x66\x31\x30\x30\x18\x64 \x01(\r\x12\x0c\n\x04\x66\x31\x30\x31\x18\x65 \x01(\r\x12\x0c\n\x04\x66\x31\x30\x32\x18\x66 \x01(\r\x12\x0c\n\x04\x66\x31\x30\x33\x18g \x01(\r\x12\x0c\n\x04\x66\x31\x30\x34\x18h \x01(\r\x12\x1d\n\x15\x63onnected_to_withings\x18i \x01(\x08\x12\x1b\n\x13\x63onnected_to_fitbit\x18j \x01(\x08\x12\x1c\n\x14launched_game_client\x18l \x01(\t\x12\x1b\n\x13\x63urrent_activity_id\x18m \x01(\x03\x12\x1b\n\x13\x63onnected_to_garmin\x18n \x01(\x08\x12*\n\treminders\x18o \x03(\x0b\x32\x17.PlayerProfile.Reminder\x12\x0c\n\x04\x66\x31\x31\x32\x18p \x01(\x08\x12&\n\x12private_attributes\x18q \x03(\x0b\x32\n.Attribute\x12%\n\x11public_attributes\x18r \x03(\x0b\x32\n.Attribute\x12\x1a\n\x12total_run_calories\x18s \x01(\x05\x12\x0c\n\x04\x66\x31\x31\x36\x18t \x01(\x03\x12\x1f\n\x17run_time_1mi_in_seconds\x18u \x01(\x05\x12\x1f\n\x17run_time_5km_in_seconds\x18v \x01(\x05\x12 \n\x18run_time_10km_in_seconds\x18w \x01(\x05\x12)\n!run_time_half_marathon_in_seconds\x18x \x01(\x05\x12)\n!run_time_full_marathon_in_seconds\x18y \x01(\x05\x12\x0c\n\x04\x66\x31\x32\x32\x18z \x01(\x05\x12@\n\x14\x63ycling_organization\x18{ \x01(\x0e\x32\".PlayerProfile.CyclingOrganization\x12\x0c\n\x04\x66\x31\x32\x34\x18| \x01(\t\x12\x36\n\x18\x64\x65\x66\x61ult_activity_privacy\x18} \x01(\x0e\x32\x14.ActivityPrivacyType\x12\x1e\n\x16\x63onnected_to_runtastic\x18~ \x01(\x08\x12)\n\x10property_changes\x18\x7f \x03(\x0b\x32\x0f.PropertyChange\x1a\xa7\x02\n\x0bSocialFacts\x12\x12\n\nprofile_id\x18\x01 \x01(\x03\x12\x17\n\x0f\x66ollowers_count\x18\x02 \x01(\x05\x12\x17\n\x0f\x66ollowees_count\x18\x03 \x01(\x05\x12\x31\n)followees_in_common_with_logged_in_player\x18\x04 \x01(\x05\x12:\n#follower_status_of_logged_in_player\x18\x05 \x01(\x0e\x32\r.FollowStatus\x12:\n#followee_status_of_logged_in_player\x18\x06 \x01(\x0e\x32\r.FollowStatus\x12\'\n\x1fis_favorite_of_logged_in_player\x18\x07 \x01(\x08\x1a\x9c\x01\n\x08Reminder\x12\n\n\x02\x66\x31\x18\x01 \x01(\x03\x12\n\n\x02\x66\x32\x18\x02 \x01(\t\x12\n\n\x02\x66\x33\x18\x03 \x01(\x03\x12\x34\n\x02\x66\x34\x18\x04 \x03(\x0b\x32(.PlayerProfile.Reminder.ReminderProperty\x1a\x36\n\x10ReminderProperty\x12\n\n\x02\x66\x31\x18\x01 \x01(\x03\x12\n\n\x02\x66\x32\x18\x02 \x01(\t\x12\n\n\x02\x66\x33\x18\x03 \x01(\t\"|\n\x0f\x45nrolledProgram\x12\x14\n\x10\x45NROLLEDPROGRAM0\x10\x00\x12\x11\n\rZWIFT_ACADEMY\x10\x01\x12\x14\n\x10\x45NROLLEDPROGRAM2\x10\x02\x12\x14\n\x10\x45NROLLEDPROGRAM3\x10\x03\x12\x14\n\x10\x45NROLLEDPROGRAM4\x10\x04\"w\n\x13\x43yclingOrganization\x12\x16\n\x12NO_CYCLING_LICENSE\x10\x00\x12\x18\n\x14\x43YCLING_SOUTH_AFRICA\x10\x01\x12\x15\n\x11\x43YCLING_AUSTRALIA\x10\x02\x12\x17\n\x13\x43YCLING_NEW_ZEALAND\x10\x03\"2\n\x0ePlayerProfiles\x12 \n\x08profiles\x18\x01 \x03(\x0b\x32\x0e.PlayerProfile\"\xb0\x06\n\x12ProfileEntitlement\x12\x31\n\x04type\x18\x01 \x01(\x0e\x32#.ProfileEntitlement.EntitlementType\x12\n\n\x02\x66\x32\x18\x02 \x01(\x03\x12<\n\x06status\x18\x03 \x01(\x0e\x32,.ProfileEntitlement.ProfileEntitlementStatus\x12\x0e\n\x06period\x18\x04 \x01(\t\x12\x17\n\x0f\x62\x65gin_time_unix\x18\x05 \x01(\r\x12\x15\n\rend_time_unix\x18\x06 \x01(\r\x12\x12\n\nkilometers\x18\x07 \x01(\r\x12\x1c\n\x14\x62\x65gin_total_distance\x18\x08 \x01(\r\x12\x1a\n\x12\x65nd_total_distance\x18\t \x01(\r\x12\x0e\n\x06source\x18\n \x01(\t\x12.\n\x08platform\x18\x0b \x01(\x0e\x32\x1c.ProfileEntitlement.Platform\x12\x19\n\x11renewal_date_unix\x18\x0c \x01(\r\x12\x18\n\x10new_trial_system\x18\r \x01(\x08\x12/\n\tplatforms\x18\x0e \x03(\x0e\x32\x1c.ProfileEntitlement.Platform\"L\n\x0f\x45ntitlementType\x12\x14\n\x10\x45NTITLEMENTTYPE0\x10\x00\x12\x08\n\x04RIDE\x10\x01\x12\x07\n\x03RUN\x10\x02\x12\x07\n\x03ROW\x10\x03\x12\x07\n\x03USE\x10\x04\"\x91\x01\n\x18ProfileEntitlementStatus\x12\x16\n\x12\x45NTITLEMENTSTATUS0\x10\x00\x12\x0b\n\x07\x45XPIRED\x10\x01\x12\n\n\x06\x41\x43TIVE\x10\x02\x12\x0c\n\x08\x43\x41NCELED\x10\x03\x12\x0c\n\x08INACTIVE\x10\x04\x12(\n$APPLIED_AS_SUBSCRIPTION_TRIAL_PERIOD\x10\x05\"\x86\x01\n\x08Platform\x12\x10\n\x0cPLATFORM_OSX\x10\x00\x12\x0f\n\x0bPLATFORM_PC\x10\x01\x12\x10\n\x0cPLATFORM_IOS\x10\x02\x12\x14\n\x10PLATFORM_ANDROID\x10\x03\x12\x11\n\rPLATFORM_TVOS\x10\x04\x12\r\n\tPLATFORM5\x10\x05\x12\r\n\tPLATFORM6\x10\x06\"\xcc\x02\n\x0cSubscription\x12&\n\x07gateway\x18\x01 \x01(\x0e\x32\x15.Subscription.Gateway\x12\x30\n\x06status\x18\x02 \x01(\x0e\x32 .Subscription.SubscriptionStatus\"#\n\x07Gateway\x12\r\n\tBRAINTREE\x10\x00\x12\t\n\x05\x41PPLE\x10\x01\"\xbc\x01\n\x12SubscriptionStatus\x12\x07\n\x03NEW\x10\x00\x12\x0b\n\x07\x45XPIRED\x10\x01\x12\n\n\x06\x41\x43TIVE\x10\x02\x12\x0c\n\x08\x43\x41NCELED\x10\x03\x12\x0c\n\x08PAST_DUE\x10\x04\x12\x0b\n\x07PENDING\x10\x05\x12\x0c\n\x08SUBERROR\x10\x06\x12\x10\n\x0cUNRECOGNIZED\x10\x07\x12\x0b\n\x07UNKNOWN\x10\x08\x12\x1f\n\x1b\x41\x43TIVE_WITH_PAYMENT_FAILURE\x10\t\x12\r\n\tABANDONED\x10\n\"\x96\x01\n\x0ePropertyChange\x12)\n\rproperty_name\x18\x01 \x02(\x0e\x32\x12.PropertyChange.Id\x12\x14\n\x0c\x63hange_count\x18\x02 \x01(\x05\x12\x13\n\x0bmax_changes\x18\x03 \x01(\x05\".\n\x02Id\x12\t\n\x05TYPE0\x10\x00\x12\x11\n\rDATE_OF_BIRTH\x10\x01\x12\n\n\x06GENDER\x10\x02\"X\n\tAttribute\x12\n\n\x02id\x18\x01 \x02(\x05\x12\x14\n\x0cnumber_value\x18\x02 \x01(\x03\x12\x13\n\x0b\x66loat_value\x18\x03 \x01(\x02\x12\x14\n\x0cstring_value\x18\x05 \x01(\t*;\n\x13\x41\x63tivityPrivacyType\x12\n\n\x06PUBLIC\x10\x00\x12\x0b\n\x07PRIVATE\x10\x01\x12\x0b\n\x07\x46RIENDS\x10\x02*E\n\x05Sport\x12\x0b\n\x07\x43YCLING\x10\x00\x12\x0b\n\x07RUNNING\x10\x01\x12\n\n\x06ROWING\x10\x02\x12\n\n\x06SPORT3\x10\x03\x12\n\n\x06SPORT4\x10\x04*\x9f\x01\n\nPlayerType\x12\x0f\n\x0bPLAYERTYPE0\x10\x00\x12\n\n\x06NORMAL\x10\x01\x12\x0f\n\x0bPRO_CYCLIST\x10\x02\x12\x0f\n\x0bZWIFT_STAFF\x10\x03\x12\x0e\n\nAMBASSADOR\x10\x04\x12\x0c\n\x08VERIFIED\x10\x05\x12\x07\n\x03ZED\x10\x06\x12\x07\n\x03ZAC\x10\x07\x12\x12\n\x0ePRO_TRIATHLETE\x10\x08\x12\x0e\n\nPRO_RUNNER\x10\t*)\n\tPowerType\x12\x0e\n\nPT_VIRTUAL\x10\x00\x12\x0c\n\x08PT_METER\x10\x01*\x9e\x01\n\x0c\x46ollowStatus\x12\x11\n\rFOLLOWSTATUS0\x10\x00\x12\x0b\n\x07UNKNOWN\x10\x01\x12\x16\n\x12REQUESTS_TO_FOLLOW\x10\x02\x12\x10\n\x0cIS_FOLLOWING\x10\x03\x12\x15\n\x11HAS_BEEN_DECLINED\x10\x07\x12\x0e\n\nIS_BLOCKED\x10\x04\x12\x13\n\x0fNO_RELATIONSHIP\x10\x05\x12\x08\n\x04SELF\x10\x06') -_PROFILEFOLLOWSTATUS = DESCRIPTOR.enum_types_by_name['ProfileFollowStatus'] -ProfileFollowStatus = enum_type_wrapper.EnumTypeWrapper(_PROFILEFOLLOWSTATUS) +_ACTIVITYPRIVACYTYPE = DESCRIPTOR.enum_types_by_name['ActivityPrivacyType'] +ActivityPrivacyType = enum_type_wrapper.EnumTypeWrapper(_ACTIVITYPRIVACYTYPE) +_SPORT = DESCRIPTOR.enum_types_by_name['Sport'] +Sport = enum_type_wrapper.EnumTypeWrapper(_SPORT) +_PLAYERTYPE = DESCRIPTOR.enum_types_by_name['PlayerType'] +PlayerType = enum_type_wrapper.EnumTypeWrapper(_PLAYERTYPE) +_POWERTYPE = DESCRIPTOR.enum_types_by_name['PowerType'] +PowerType = enum_type_wrapper.EnumTypeWrapper(_POWERTYPE) +_FOLLOWSTATUS = DESCRIPTOR.enum_types_by_name['FollowStatus'] +FollowStatus = enum_type_wrapper.EnumTypeWrapper(_FOLLOWSTATUS) +PUBLIC = 0 +PRIVATE = 1 +FRIENDS = 2 +CYCLING = 0 +RUNNING = 1 +ROWING = 2 +SPORT3 = 3 +SPORT4 = 4 +PLAYERTYPE0 = 0 +NORMAL = 1 +PRO_CYCLIST = 2 +ZWIFT_STAFF = 3 +AMBASSADOR = 4 +VERIFIED = 5 +ZED = 6 +ZAC = 7 +PRO_TRIATHLETE = 8 +PRO_RUNNER = 9 +PT_VIRTUAL = 0 +PT_METER = 1 FOLLOWSTATUS0 = 0 -SELF = 1 -FOLLOWSTATUS2 = 2 -FOLLOWSTATUS3 = 3 -FOLLOWSTATUS4 = 4 +UNKNOWN = 1 +REQUESTS_TO_FOLLOW = 2 +IS_FOLLOWING = 3 +HAS_BEEN_DECLINED = 7 +IS_BLOCKED = 4 +NO_RELATIONSHIP = 5 +SELF = 6 -_PROFILE = DESCRIPTOR.message_types_by_name['Profile'] -_PROFILE_PROFILESOCIALFACTS = _PROFILE.nested_types_by_name['ProfileSocialFacts'] -_PROFILES = DESCRIPTOR.message_types_by_name['Profiles'] +_ACHIEVEMENTENTRY = DESCRIPTOR.message_types_by_name['AchievementEntry'] +_ACHIEVEMENTS = DESCRIPTOR.message_types_by_name['Achievements'] +_PLAYERPROFILE = DESCRIPTOR.message_types_by_name['PlayerProfile'] +_PLAYERPROFILE_SOCIALFACTS = _PLAYERPROFILE.nested_types_by_name['SocialFacts'] +_PLAYERPROFILE_REMINDER = _PLAYERPROFILE.nested_types_by_name['Reminder'] +_PLAYERPROFILE_REMINDER_REMINDERPROPERTY = _PLAYERPROFILE_REMINDER.nested_types_by_name['ReminderProperty'] +_PLAYERPROFILES = DESCRIPTOR.message_types_by_name['PlayerProfiles'] _PROFILEENTITLEMENT = DESCRIPTOR.message_types_by_name['ProfileEntitlement'] _SUBSCRIPTION = DESCRIPTOR.message_types_by_name['Subscription'] -_PACERSETTING = DESCRIPTOR.message_types_by_name['PacerSetting'] -_PROFILE_PLAYERTYPE = _PROFILE.enum_types_by_name['PlayerType'] -_PROFILE_PROFILEENROLLEDPROGRAM = _PROFILE.enum_types_by_name['ProfileEnrolledProgram'] -_PROFILE_SPORT = _PROFILE.enum_types_by_name['Sport'] +_PROPERTYCHANGE = DESCRIPTOR.message_types_by_name['PropertyChange'] +_ATTRIBUTE = DESCRIPTOR.message_types_by_name['Attribute'] +_PLAYERPROFILE_ENROLLEDPROGRAM = _PLAYERPROFILE.enum_types_by_name['EnrolledProgram'] +_PLAYERPROFILE_CYCLINGORGANIZATION = _PLAYERPROFILE.enum_types_by_name['CyclingOrganization'] _PROFILEENTITLEMENT_ENTITLEMENTTYPE = _PROFILEENTITLEMENT.enum_types_by_name['EntitlementType'] _PROFILEENTITLEMENT_PROFILEENTITLEMENTSTATUS = _PROFILEENTITLEMENT.enum_types_by_name['ProfileEntitlementStatus'] _PROFILEENTITLEMENT_PLATFORM = _PROFILEENTITLEMENT.enum_types_by_name['Platform'] _SUBSCRIPTION_GATEWAY = _SUBSCRIPTION.enum_types_by_name['Gateway'] _SUBSCRIPTION_SUBSCRIPTIONSTATUS = _SUBSCRIPTION.enum_types_by_name['SubscriptionStatus'] -Profile = _reflection.GeneratedProtocolMessageType('Profile', (_message.Message,), { +_PROPERTYCHANGE_ID = _PROPERTYCHANGE.enum_types_by_name['Id'] +AchievementEntry = _reflection.GeneratedProtocolMessageType('AchievementEntry', (_message.Message,), { + 'DESCRIPTOR' : _ACHIEVEMENTENTRY, + '__module__' : 'profile_pb2' + # @@protoc_insertion_point(class_scope:AchievementEntry) + }) +_sym_db.RegisterMessage(AchievementEntry) - 'ProfileSocialFacts' : _reflection.GeneratedProtocolMessageType('ProfileSocialFacts', (_message.Message,), { - 'DESCRIPTOR' : _PROFILE_PROFILESOCIALFACTS, +Achievements = _reflection.GeneratedProtocolMessageType('Achievements', (_message.Message,), { + 'DESCRIPTOR' : _ACHIEVEMENTS, + '__module__' : 'profile_pb2' + # @@protoc_insertion_point(class_scope:Achievements) + }) +_sym_db.RegisterMessage(Achievements) + +PlayerProfile = _reflection.GeneratedProtocolMessageType('PlayerProfile', (_message.Message,), { + + 'SocialFacts' : _reflection.GeneratedProtocolMessageType('SocialFacts', (_message.Message,), { + 'DESCRIPTOR' : _PLAYERPROFILE_SOCIALFACTS, '__module__' : 'profile_pb2' - # @@protoc_insertion_point(class_scope:Profile.ProfileSocialFacts) + # @@protoc_insertion_point(class_scope:PlayerProfile.SocialFacts) }) , - 'DESCRIPTOR' : _PROFILE, - '__module__' : 'profile_pb2' - # @@protoc_insertion_point(class_scope:Profile) - }) -_sym_db.RegisterMessage(Profile) -_sym_db.RegisterMessage(Profile.ProfileSocialFacts) -Profiles = _reflection.GeneratedProtocolMessageType('Profiles', (_message.Message,), { - 'DESCRIPTOR' : _PROFILES, + 'Reminder' : _reflection.GeneratedProtocolMessageType('Reminder', (_message.Message,), { + + 'ReminderProperty' : _reflection.GeneratedProtocolMessageType('ReminderProperty', (_message.Message,), { + 'DESCRIPTOR' : _PLAYERPROFILE_REMINDER_REMINDERPROPERTY, + '__module__' : 'profile_pb2' + # @@protoc_insertion_point(class_scope:PlayerProfile.Reminder.ReminderProperty) + }) + , + 'DESCRIPTOR' : _PLAYERPROFILE_REMINDER, + '__module__' : 'profile_pb2' + # @@protoc_insertion_point(class_scope:PlayerProfile.Reminder) + }) + , + 'DESCRIPTOR' : _PLAYERPROFILE, '__module__' : 'profile_pb2' - # @@protoc_insertion_point(class_scope:Profiles) + # @@protoc_insertion_point(class_scope:PlayerProfile) }) -_sym_db.RegisterMessage(Profiles) +_sym_db.RegisterMessage(PlayerProfile) +_sym_db.RegisterMessage(PlayerProfile.SocialFacts) +_sym_db.RegisterMessage(PlayerProfile.Reminder) +_sym_db.RegisterMessage(PlayerProfile.Reminder.ReminderProperty) + +PlayerProfiles = _reflection.GeneratedProtocolMessageType('PlayerProfiles', (_message.Message,), { + 'DESCRIPTOR' : _PLAYERPROFILES, + '__module__' : 'profile_pb2' + # @@protoc_insertion_point(class_scope:PlayerProfiles) + }) +_sym_db.RegisterMessage(PlayerProfiles) ProfileEntitlement = _reflection.GeneratedProtocolMessageType('ProfileEntitlement', (_message.Message,), { 'DESCRIPTOR' : _PROFILEENTITLEMENT, @@ -76,44 +142,69 @@ Subscription = _reflection.GeneratedProtocolMessageType('Subscription', (_messag }) _sym_db.RegisterMessage(Subscription) -PacerSetting = _reflection.GeneratedProtocolMessageType('PacerSetting', (_message.Message,), { - 'DESCRIPTOR' : _PACERSETTING, +PropertyChange = _reflection.GeneratedProtocolMessageType('PropertyChange', (_message.Message,), { + 'DESCRIPTOR' : _PROPERTYCHANGE, '__module__' : 'profile_pb2' - # @@protoc_insertion_point(class_scope:PacerSetting) + # @@protoc_insertion_point(class_scope:PropertyChange) }) -_sym_db.RegisterMessage(PacerSetting) +_sym_db.RegisterMessage(PropertyChange) + +Attribute = _reflection.GeneratedProtocolMessageType('Attribute', (_message.Message,), { + 'DESCRIPTOR' : _ATTRIBUTE, + '__module__' : 'profile_pb2' + # @@protoc_insertion_point(class_scope:Attribute) + }) +_sym_db.RegisterMessage(Attribute) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None - _PROFILEFOLLOWSTATUS._serialized_start=3522 - _PROFILEFOLLOWSTATUS._serialized_end=3629 - _PROFILE._serialized_start=18 - _PROFILE._serialized_end=2399 - _PROFILE_PROFILESOCIALFACTS._serialized_start=1943 - _PROFILE_PROFILESOCIALFACTS._serialized_end=2099 - _PROFILE_PLAYERTYPE._serialized_start=2101 - _PROFILE_PLAYERTYPE._serialized_end=2193 - _PROFILE_PROFILEENROLLEDPROGRAM._serialized_start=2196 - _PROFILE_PROFILEENROLLEDPROGRAM._serialized_end=2330 - _PROFILE_SPORT._serialized_start=2332 - _PROFILE_SPORT._serialized_end=2399 - _PROFILES._serialized_start=2401 - _PROFILES._serialized_end=2439 - _PROFILEENTITLEMENT._serialized_start=2442 - _PROFILEENTITLEMENT._serialized_end=3129 - _PROFILEENTITLEMENT_ENTITLEMENTTYPE._serialized_start=2736 - _PROFILEENTITLEMENT_ENTITLEMENTTYPE._serialized_end=2863 - _PROFILEENTITLEMENT_PROFILEENTITLEMENTSTATUS._serialized_start=2866 - _PROFILEENTITLEMENT_PROFILEENTITLEMENTSTATUS._serialized_end=3012 - _PROFILEENTITLEMENT_PLATFORM._serialized_start=3014 - _PROFILEENTITLEMENT_PLATFORM._serialized_end=3129 - _SUBSCRIPTION._serialized_start=3132 - _SUBSCRIPTION._serialized_end=3429 - _SUBSCRIPTION_GATEWAY._serialized_start=3229 - _SUBSCRIPTION_GATEWAY._serialized_end=3308 - _SUBSCRIPTION_SUBSCRIPTIONSTATUS._serialized_start=3310 - _SUBSCRIPTION_SUBSCRIPTIONSTATUS._serialized_end=3429 - _PACERSETTING._serialized_start=3431 - _PACERSETTING._serialized_end=3520 + _ACTIVITYPRIVACYTYPE._serialized_start=5355 + _ACTIVITYPRIVACYTYPE._serialized_end=5414 + _SPORT._serialized_start=5416 + _SPORT._serialized_end=5485 + _PLAYERTYPE._serialized_start=5488 + _PLAYERTYPE._serialized_end=5647 + _POWERTYPE._serialized_start=5649 + _POWERTYPE._serialized_end=5690 + _FOLLOWSTATUS._serialized_start=5693 + _FOLLOWSTATUS._serialized_end=5851 + _ACHIEVEMENTENTRY._serialized_start=17 + _ACHIEVEMENTENTRY._serialized_end=47 + _ACHIEVEMENTS._serialized_start=49 + _ACHIEVEMENTS._serialized_end=104 + _PLAYERPROFILE._serialized_start=107 + _PLAYERPROFILE._serialized_end=3904 + _PLAYERPROFILE_SOCIALFACTS._serialized_start=3203 + _PLAYERPROFILE_SOCIALFACTS._serialized_end=3498 + _PLAYERPROFILE_REMINDER._serialized_start=3501 + _PLAYERPROFILE_REMINDER._serialized_end=3657 + _PLAYERPROFILE_REMINDER_REMINDERPROPERTY._serialized_start=3603 + _PLAYERPROFILE_REMINDER_REMINDERPROPERTY._serialized_end=3657 + _PLAYERPROFILE_ENROLLEDPROGRAM._serialized_start=3659 + _PLAYERPROFILE_ENROLLEDPROGRAM._serialized_end=3783 + _PLAYERPROFILE_CYCLINGORGANIZATION._serialized_start=3785 + _PLAYERPROFILE_CYCLINGORGANIZATION._serialized_end=3904 + _PLAYERPROFILES._serialized_start=3906 + _PLAYERPROFILES._serialized_end=3956 + _PROFILEENTITLEMENT._serialized_start=3959 + _PROFILEENTITLEMENT._serialized_end=4775 + _PROFILEENTITLEMENT_ENTITLEMENTTYPE._serialized_start=4414 + _PROFILEENTITLEMENT_ENTITLEMENTTYPE._serialized_end=4490 + _PROFILEENTITLEMENT_PROFILEENTITLEMENTSTATUS._serialized_start=4493 + _PROFILEENTITLEMENT_PROFILEENTITLEMENTSTATUS._serialized_end=4638 + _PROFILEENTITLEMENT_PLATFORM._serialized_start=4641 + _PROFILEENTITLEMENT_PLATFORM._serialized_end=4775 + _SUBSCRIPTION._serialized_start=4778 + _SUBSCRIPTION._serialized_end=5110 + _SUBSCRIPTION_GATEWAY._serialized_start=4884 + _SUBSCRIPTION_GATEWAY._serialized_end=4919 + _SUBSCRIPTION_SUBSCRIPTIONSTATUS._serialized_start=4922 + _SUBSCRIPTION_SUBSCRIPTIONSTATUS._serialized_end=5110 + _PROPERTYCHANGE._serialized_start=5113 + _PROPERTYCHANGE._serialized_end=5263 + _PROPERTYCHANGE_ID._serialized_start=5217 + _PROPERTYCHANGE_ID._serialized_end=5263 + _ATTRIBUTE._serialized_start=5265 + _ATTRIBUTE._serialized_end=5353 # @@protoc_insertion_point(module_scope) diff --git a/protobuf/segment-result.proto b/protobuf/segment-result.proto index 0ad5d8a..f8f49af 100644 --- a/protobuf/segment-result.proto +++ b/protobuf/segment-result.proto @@ -1,9 +1,11 @@ syntax = "proto2"; +//import "profile.proto"; //enums PlayerType and Sport +//TODO: incorporate new fields, rename db fields message SegmentResult { optional uint64 id = 1; required uint64 player_id = 2; - optional uint32 f3 = 3; - optional uint32 f4 = 4; + optional uint64 f3 = 3; //=g_CurrentServerRealmID + optional uint64 f4 = 4; //=CourseID optional uint64 segment_id = 5; optional uint64 event_subgroup_id = 6; required string first_name = 7; @@ -11,19 +13,22 @@ message SegmentResult { optional uint64 world_time = 9; optional string finish_time_str = 10; required uint64 elapsed_ms = 11; - optional bool f12 = 12; + optional int64 f12 = 12; //-> enum PowerType + '-1'? (in ZNETWORK_RegisterPlayerSegmentResult from m_bikeEntity->m_hasPowerMeter) optional uint32 f13 = 13; //weight_in_grams - optional uint32 f14 = 14; + optional uint32 f14 = 14; //:=0 in Leaderboards::SetPlayerSegmentResult optional uint32 f15 = 15; //avg_power - optional bool f16 = 16; - optional string f17 = 17; - optional uint64 f18 = 18; - optional uint32 f19 = 19; - optional uint32 f20 = 20; + optional bool f16 = 16; //:= isMale + optional string f17 = 17; //ISO8601 time (magicLeaderboardBirthday := const @ ZNETWORK_Initialize) + optional uint64 f18 = 18; //-> enum PlayerType player_type + optional uint32 f19 = 19; //avg_hr(ZNETWORK_RaceResultEntrySaveRequest):=m_computer.m_accumHeartRate/m_computer.m_accumTime @ZNETWORK_RegisterLocalPlayersSegmentResult (or 0.0) + optional int64 f20 = 20; //-> enum Sport sport + //optional uint64 f21 = 21; //:=activityId (may be -1) + //optional bool f22 = 22; + //optional string f23 = 23; } message SegmentResults { - required uint32 world_id = 1; + required uint64 server_realm = 1; required uint64 segment_id = 2; optional uint64 event_subgroup_id = 3; repeated SegmentResult segment_results = 4; diff --git a/protobuf/segment_result_pb2.py b/protobuf/segment_result_pb2.py index 5da72a6..a3e9af5 100644 --- a/protobuf/segment_result_pb2.py +++ b/protobuf/segment_result_pb2.py @@ -14,7 +14,7 @@ _sym_db = _symbol_database.Default() -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x14segment-result.proto\"\xd2\x02\n\rSegmentResult\x12\n\n\x02id\x18\x01 \x01(\x04\x12\x11\n\tplayer_id\x18\x02 \x02(\x04\x12\n\n\x02\x66\x33\x18\x03 \x01(\r\x12\n\n\x02\x66\x34\x18\x04 \x01(\r\x12\x12\n\nsegment_id\x18\x05 \x01(\x04\x12\x19\n\x11\x65vent_subgroup_id\x18\x06 \x01(\x04\x12\x12\n\nfirst_name\x18\x07 \x02(\t\x12\x11\n\tlast_name\x18\x08 \x02(\t\x12\x12\n\nworld_time\x18\t \x01(\x04\x12\x17\n\x0f\x66inish_time_str\x18\n \x01(\t\x12\x12\n\nelapsed_ms\x18\x0b \x02(\x04\x12\x0b\n\x03\x66\x31\x32\x18\x0c \x01(\x08\x12\x0b\n\x03\x66\x31\x33\x18\r \x01(\r\x12\x0b\n\x03\x66\x31\x34\x18\x0e \x01(\r\x12\x0b\n\x03\x66\x31\x35\x18\x0f \x01(\r\x12\x0b\n\x03\x66\x31\x36\x18\x10 \x01(\x08\x12\x0b\n\x03\x66\x31\x37\x18\x11 \x01(\t\x12\x0b\n\x03\x66\x31\x38\x18\x12 \x01(\x04\x12\x0b\n\x03\x66\x31\x39\x18\x13 \x01(\r\x12\x0b\n\x03\x66\x32\x30\x18\x14 \x01(\r\"z\n\x0eSegmentResults\x12\x10\n\x08world_id\x18\x01 \x02(\r\x12\x12\n\nsegment_id\x18\x02 \x02(\x04\x12\x19\n\x11\x65vent_subgroup_id\x18\x03 \x01(\x04\x12\'\n\x0fsegment_results\x18\x04 \x03(\x0b\x32\x0e.SegmentResult') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x14segment-result.proto\"\xd2\x02\n\rSegmentResult\x12\n\n\x02id\x18\x01 \x01(\x04\x12\x11\n\tplayer_id\x18\x02 \x02(\x04\x12\n\n\x02\x66\x33\x18\x03 \x01(\x04\x12\n\n\x02\x66\x34\x18\x04 \x01(\x04\x12\x12\n\nsegment_id\x18\x05 \x01(\x04\x12\x19\n\x11\x65vent_subgroup_id\x18\x06 \x01(\x04\x12\x12\n\nfirst_name\x18\x07 \x02(\t\x12\x11\n\tlast_name\x18\x08 \x02(\t\x12\x12\n\nworld_time\x18\t \x01(\x04\x12\x17\n\x0f\x66inish_time_str\x18\n \x01(\t\x12\x12\n\nelapsed_ms\x18\x0b \x02(\x04\x12\x0b\n\x03\x66\x31\x32\x18\x0c \x01(\x03\x12\x0b\n\x03\x66\x31\x33\x18\r \x01(\r\x12\x0b\n\x03\x66\x31\x34\x18\x0e \x01(\r\x12\x0b\n\x03\x66\x31\x35\x18\x0f \x01(\r\x12\x0b\n\x03\x66\x31\x36\x18\x10 \x01(\x08\x12\x0b\n\x03\x66\x31\x37\x18\x11 \x01(\t\x12\x0b\n\x03\x66\x31\x38\x18\x12 \x01(\x04\x12\x0b\n\x03\x66\x31\x39\x18\x13 \x01(\r\x12\x0b\n\x03\x66\x32\x30\x18\x14 \x01(\x03\"~\n\x0eSegmentResults\x12\x14\n\x0cserver_realm\x18\x01 \x02(\x04\x12\x12\n\nsegment_id\x18\x02 \x02(\x04\x12\x19\n\x11\x65vent_subgroup_id\x18\x03 \x01(\x04\x12\'\n\x0fsegment_results\x18\x04 \x03(\x0b\x32\x0e.SegmentResult') @@ -40,5 +40,5 @@ if _descriptor._USE_C_DESCRIPTORS == False: _SEGMENTRESULT._serialized_start=25 _SEGMENTRESULT._serialized_end=363 _SEGMENTRESULTS._serialized_start=365 - _SEGMENTRESULTS._serialized_end=487 + _SEGMENTRESULTS._serialized_end=491 # @@protoc_insertion_point(module_scope) diff --git a/protobuf/tcp-node-msgs.proto b/protobuf/tcp-node-msgs.proto index e4d1ed5..2d8b6d8 100644 --- a/protobuf/tcp-node-msgs.proto +++ b/protobuf/tcp-node-msgs.proto @@ -1,53 +1,116 @@ syntax = "proto2"; -//////////////////////////////////////// -// Initial TCP response below -//////////////////////////////////////// - -message ServerDetails { - required int32 f1 = 1; - required int32 f2 = 2; - required string ip = 3; - required int32 port = 4; +enum SocialPlayerActionType { + SOCIAL_ACTION_UNKNOWN_TYPE = 0; + SOCIAL_TEXT_MESSAGE = 1; + SOCIAL_RIDE_ON = 2; + SOCIAL_FLAG = 3; +} +enum FlagType { + FLAG_TYPE_UNKNOWN = 0; + FLAG_TYPE_HARASSMENT = 1; + FLAG_TYPE_FLIER = 2; + FLAG_TYPE_BAD_LANGUAGE = 3; +} +enum MessageGroupType { + MGT_UNKNOWN = 0; + MGT_GLOBAL = 1; + MGT_DIRECT = 2; + MGT_EVENT = 3; + MGT_CLUB = 4; +} +message SocialPlayerAction { + optional int64 player_id = 1; + optional int64 to_player_id = 2; // 0 if public message + optional SocialPlayerActionType spa_type = 3; + optional string firstName = 4; + optional string lastName = 5; + optional string message = 6; + optional string avatar = 7; + optional int32 countryCode = 8; + optional FlagType flagType = 9; + optional MessageGroupType mgType = 10; + optional int64 eventSubgroup = 11; +} +/*message MobileAlertResponse { + optional int64 f1 = 1; + optional int64 f2 = 2; } -message ServersType1 { - repeated ServerDetails details = 1; - optional int32 f2 = 2; - optional int32 f3 = 3; - optional int32 f4 = 4; -} +message BLEPeripheralCharacteristic { + optional string f1 = 1; + optional bytes f2 = 2; +}*/ -message ServerConnectionDetailsWrapper { - required int32 f1 = 1; // Value seems to be the same as ConnDetails' f1 - required int32 f2 = 2; // Value seems to be the same as ConnDetails' f2 - repeated ServerDetails details = 3; -} +//TODO: PeripheralResponseType UNKNOWN_RESPONSE_TYPE(0), PERIPHERAL_ERROR(1), CHARACTERISTIC_VALUE(2), PERIPHERAL_CONNECTED(3), PERIPHERAL_DISCONNECTED(4), PERIPHERAL_DISCOVERED(5); +// PeripheralErrorType UNKNOWN_ERROR(0), PERMISSION_DENIED(1), BLE_UNSUPPORTED(2), BLE_POWERED_OFF(3); +/*message BLEPeripheralResponse { + optional uint32 f1 = 1; + optional uint32 f2 = 2; + optional string f3 = 3; + optional BLEPeripheral f4 = 4; + optional BLEPeripheralCharacteristic f5 = 5; +}*/ -message ServersType2 { - repeated ServerConnectionDetailsWrapper details_wrapper = 1; - required int32 port = 2; -} - -// login-response.proto already defines ServerInfo and Python 3 doesn't like reusing it -message TCPServerInfo { +/*TODO: UNKNOWN(0), FLAGS(1), INCOMPLETE_UUIDS_16_BIT(2), COMPLETE_UUIDS_16_BIT(3), INCOMPLETE_UUIDS_32_BIT(4), + COMPLETE_UUIDS_32_BIT(5), INCOMPLETE_UUIDS_128_BIT(6), COMPLETE_UUIDS_128_BIT(7), SHORTENED_LOCAL_NAME(8), + COMPLETE_LOCAL_NAME(9), TX_POWER_LEVEL(10), CLASS_OF_DEVICE(13), SIMPLE_PAIRING_HASH(14), SIMPLE_PAIRING_RANDOMIZER(15), + DEVICE_ID(16), SECURITY_MANAGER_OOB_FLAGS(17), SLAVE_CONNECTION_INTERVAL(18), SOLICITATION_UUIDS_16_BIT(20), + SOLICITATION_UUIDS_128_BIT(21), SERVICE_DATA_16_BIT(22), PUBLIC_TARGET_ADDRESS(23), RANDOM_TARGET_ADDRESS(24), + APPEARANCE(25), ADVERTISING_INTERVAL(26), LE_BLUETOOTH_DEVICE_ADDRESS(27), LE_ROLE(28), SIMPLE_PAIRING_HASH_C256(29), + SIMPLE_PAIRING_RANDOMIZER_R256(30), SOLICITATION_UUIDS_32_BIT(31), SERVICE_DATA_32_BIT(32), SERVICE_DATA_128_BIT(33), + LE_SECURE_CONFIRMATION_VALUE(34), LE_SECURE_RANDOM_VALUE(35), URI(36), INDOOR_POSITIONING(37), TRANSPORT_DISCOVERY_DATA(38), + LE_SUPPORTED_FEATURES(39), CHANNEL_MAP_UPDATE_INDICATION(40), PB_ADV(41), MESH_MESSAGE(42), MESH_BEACON(43), BIG_INFO(44), + BROADCAST_CODE(45), INFORMATION_DATA_3D(61), MANUFACTURER_DATA(255); */ +/*message BLEAdvertisementDataSection { optional int32 f1 = 1; - required int32 player_id = 2; - required int32 f3 = 3; - repeated ServersType1 servers = 24; - repeated ServersType2 other_servers = 25; + optional bytes f2 = 2; } -message TCPHello { - required int32 player_id = 2; -} - -//////////////////////////////////////// -// Recurring TCP response below -//////////////////////////////////////// - -message RecurringTCPResponse { - optional int32 f1 = 1; - optional int32 player_id = 2; +message BLEPeripheral { + optional string f1 = 1; + optional string f2 = 2; + optional int32 f3 = 3; +}*/ + +/*TODO: CONNECTABLE_UNDIRECTED(0), CONNECTABLE_DIRECTED(1), SCANNABLE_UNDIRECTED(2), NON_CONNECTABLE_UNDIRECTED(3), + SCAN_RESPONSE(4), EXTENDED(5); */ +/*message BLEAdvertisement { + optional BLEPeripheral f1 = 1; + repeated BLEAdvertisementDataSection f2 = 2; optional int32 f3 = 3; - optional int32 f11 = 11; } + +message PhoneToGameCommand { + required int32 seqno = 1; + required uint32 command = 2; + optional int64 f3 = 3; + optional string f4 = 4; + optional int64 f5 = 5; + optional string f6 = 6; + optional int64 f7 = 7; + optional int32 f8 = 8; + optional uint32 f9 = 9; + required uint32 command_copy = 10; + optional SocialPlayerAction f11 = 11; + // no 12 + optional MobileAlertResponse f13 = 13; + // no 14-17 + optional BLEPeripheralResponse f18 = 18; + optional int64 f19 = 19; + optional string f20 = 20; + optional bytes f21 = 21; + optional BLEAdvertisement f22 = 22; +} + +message PhoneToGame { + required int64 player_id = 1; + repeated PhoneToGameCommand command = 2; + optional float f3 = 3; + optional float f4 = 4; + optional float f5 = 5; + optional float f6 = 6; + optional float f7 = 7; + optional float f8 = 8; + optional double f9 = 9; + optional int32 f10 = 10; +}*/ \ No newline at end of file diff --git a/protobuf/tcp_node_msgs_pb2.py b/protobuf/tcp_node_msgs_pb2.py index e710d7a..36097d2 100644 --- a/protobuf/tcp_node_msgs_pb2.py +++ b/protobuf/tcp_node_msgs_pb2.py @@ -2,6 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # source: tcp-node-msgs.proto """Generated protocol buffer code.""" +from google.protobuf.internal import enum_type_wrapper from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import message as _message @@ -14,81 +15,46 @@ _sym_db = _symbol_database.Default() -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x13tcp-node-msgs.proto\"A\n\rServerDetails\x12\n\n\x02\x66\x31\x18\x01 \x02(\x05\x12\n\n\x02\x66\x32\x18\x02 \x02(\x05\x12\n\n\x02ip\x18\x03 \x02(\t\x12\x0c\n\x04port\x18\x04 \x02(\x05\"S\n\x0cServersType1\x12\x1f\n\x07\x64\x65tails\x18\x01 \x03(\x0b\x32\x0e.ServerDetails\x12\n\n\x02\x66\x32\x18\x02 \x01(\x05\x12\n\n\x02\x66\x33\x18\x03 \x01(\x05\x12\n\n\x02\x66\x34\x18\x04 \x01(\x05\"Y\n\x1eServerConnectionDetailsWrapper\x12\n\n\x02\x66\x31\x18\x01 \x02(\x05\x12\n\n\x02\x66\x32\x18\x02 \x02(\x05\x12\x1f\n\x07\x64\x65tails\x18\x03 \x03(\x0b\x32\x0e.ServerDetails\"V\n\x0cServersType2\x12\x38\n\x0f\x64\x65tails_wrapper\x18\x01 \x03(\x0b\x32\x1f.ServerConnectionDetailsWrapper\x12\x0c\n\x04port\x18\x02 \x02(\x05\"\x80\x01\n\rTCPServerInfo\x12\n\n\x02\x66\x31\x18\x01 \x01(\x05\x12\x11\n\tplayer_id\x18\x02 \x02(\x05\x12\n\n\x02\x66\x33\x18\x03 \x02(\x05\x12\x1e\n\x07servers\x18\x18 \x03(\x0b\x32\r.ServersType1\x12$\n\rother_servers\x18\x19 \x03(\x0b\x32\r.ServersType2\"\x1d\n\x08TCPHello\x12\x11\n\tplayer_id\x18\x02 \x02(\x05\"N\n\x14RecurringTCPResponse\x12\n\n\x02\x66\x31\x18\x01 \x01(\x05\x12\x11\n\tplayer_id\x18\x02 \x01(\x05\x12\n\n\x02\x66\x33\x18\x03 \x01(\x05\x12\x0b\n\x03\x66\x31\x31\x18\x0b \x01(\x05') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x13tcp-node-msgs.proto\"\x9a\x02\n\x12SocialPlayerAction\x12\x11\n\tplayer_id\x18\x01 \x01(\x03\x12\x14\n\x0cto_player_id\x18\x02 \x01(\x03\x12)\n\x08spa_type\x18\x03 \x01(\x0e\x32\x17.SocialPlayerActionType\x12\x11\n\tfirstName\x18\x04 \x01(\t\x12\x10\n\x08lastName\x18\x05 \x01(\t\x12\x0f\n\x07message\x18\x06 \x01(\t\x12\x0e\n\x06\x61vatar\x18\x07 \x01(\t\x12\x13\n\x0b\x63ountryCode\x18\x08 \x01(\x05\x12\x1b\n\x08\x66lagType\x18\t \x01(\x0e\x32\t.FlagType\x12!\n\x06mgType\x18\n \x01(\x0e\x32\x11.MessageGroupType\x12\x15\n\reventSubgroup\x18\x0b \x01(\x03*v\n\x16SocialPlayerActionType\x12\x1e\n\x1aSOCIAL_ACTION_UNKNOWN_TYPE\x10\x00\x12\x17\n\x13SOCIAL_TEXT_MESSAGE\x10\x01\x12\x12\n\x0eSOCIAL_RIDE_ON\x10\x02\x12\x0f\n\x0bSOCIAL_FLAG\x10\x03*l\n\x08\x46lagType\x12\x15\n\x11\x46LAG_TYPE_UNKNOWN\x10\x00\x12\x18\n\x14\x46LAG_TYPE_HARASSMENT\x10\x01\x12\x13\n\x0f\x46LAG_TYPE_FLIER\x10\x02\x12\x1a\n\x16\x46LAG_TYPE_BAD_LANGUAGE\x10\x03*`\n\x10MessageGroupType\x12\x0f\n\x0bMGT_UNKNOWN\x10\x00\x12\x0e\n\nMGT_GLOBAL\x10\x01\x12\x0e\n\nMGT_DIRECT\x10\x02\x12\r\n\tMGT_EVENT\x10\x03\x12\x0c\n\x08MGT_CLUB\x10\x04') + +_SOCIALPLAYERACTIONTYPE = DESCRIPTOR.enum_types_by_name['SocialPlayerActionType'] +SocialPlayerActionType = enum_type_wrapper.EnumTypeWrapper(_SOCIALPLAYERACTIONTYPE) +_FLAGTYPE = DESCRIPTOR.enum_types_by_name['FlagType'] +FlagType = enum_type_wrapper.EnumTypeWrapper(_FLAGTYPE) +_MESSAGEGROUPTYPE = DESCRIPTOR.enum_types_by_name['MessageGroupType'] +MessageGroupType = enum_type_wrapper.EnumTypeWrapper(_MESSAGEGROUPTYPE) +SOCIAL_ACTION_UNKNOWN_TYPE = 0 +SOCIAL_TEXT_MESSAGE = 1 +SOCIAL_RIDE_ON = 2 +SOCIAL_FLAG = 3 +FLAG_TYPE_UNKNOWN = 0 +FLAG_TYPE_HARASSMENT = 1 +FLAG_TYPE_FLIER = 2 +FLAG_TYPE_BAD_LANGUAGE = 3 +MGT_UNKNOWN = 0 +MGT_GLOBAL = 1 +MGT_DIRECT = 2 +MGT_EVENT = 3 +MGT_CLUB = 4 - -_SERVERDETAILS = DESCRIPTOR.message_types_by_name['ServerDetails'] -_SERVERSTYPE1 = DESCRIPTOR.message_types_by_name['ServersType1'] -_SERVERCONNECTIONDETAILSWRAPPER = DESCRIPTOR.message_types_by_name['ServerConnectionDetailsWrapper'] -_SERVERSTYPE2 = DESCRIPTOR.message_types_by_name['ServersType2'] -_TCPSERVERINFO = DESCRIPTOR.message_types_by_name['TCPServerInfo'] -_TCPHELLO = DESCRIPTOR.message_types_by_name['TCPHello'] -_RECURRINGTCPRESPONSE = DESCRIPTOR.message_types_by_name['RecurringTCPResponse'] -ServerDetails = _reflection.GeneratedProtocolMessageType('ServerDetails', (_message.Message,), { - 'DESCRIPTOR' : _SERVERDETAILS, +_SOCIALPLAYERACTION = DESCRIPTOR.message_types_by_name['SocialPlayerAction'] +SocialPlayerAction = _reflection.GeneratedProtocolMessageType('SocialPlayerAction', (_message.Message,), { + 'DESCRIPTOR' : _SOCIALPLAYERACTION, '__module__' : 'tcp_node_msgs_pb2' - # @@protoc_insertion_point(class_scope:ServerDetails) + # @@protoc_insertion_point(class_scope:SocialPlayerAction) }) -_sym_db.RegisterMessage(ServerDetails) - -ServersType1 = _reflection.GeneratedProtocolMessageType('ServersType1', (_message.Message,), { - 'DESCRIPTOR' : _SERVERSTYPE1, - '__module__' : 'tcp_node_msgs_pb2' - # @@protoc_insertion_point(class_scope:ServersType1) - }) -_sym_db.RegisterMessage(ServersType1) - -ServerConnectionDetailsWrapper = _reflection.GeneratedProtocolMessageType('ServerConnectionDetailsWrapper', (_message.Message,), { - 'DESCRIPTOR' : _SERVERCONNECTIONDETAILSWRAPPER, - '__module__' : 'tcp_node_msgs_pb2' - # @@protoc_insertion_point(class_scope:ServerConnectionDetailsWrapper) - }) -_sym_db.RegisterMessage(ServerConnectionDetailsWrapper) - -ServersType2 = _reflection.GeneratedProtocolMessageType('ServersType2', (_message.Message,), { - 'DESCRIPTOR' : _SERVERSTYPE2, - '__module__' : 'tcp_node_msgs_pb2' - # @@protoc_insertion_point(class_scope:ServersType2) - }) -_sym_db.RegisterMessage(ServersType2) - -TCPServerInfo = _reflection.GeneratedProtocolMessageType('TCPServerInfo', (_message.Message,), { - 'DESCRIPTOR' : _TCPSERVERINFO, - '__module__' : 'tcp_node_msgs_pb2' - # @@protoc_insertion_point(class_scope:TCPServerInfo) - }) -_sym_db.RegisterMessage(TCPServerInfo) - -TCPHello = _reflection.GeneratedProtocolMessageType('TCPHello', (_message.Message,), { - 'DESCRIPTOR' : _TCPHELLO, - '__module__' : 'tcp_node_msgs_pb2' - # @@protoc_insertion_point(class_scope:TCPHello) - }) -_sym_db.RegisterMessage(TCPHello) - -RecurringTCPResponse = _reflection.GeneratedProtocolMessageType('RecurringTCPResponse', (_message.Message,), { - 'DESCRIPTOR' : _RECURRINGTCPRESPONSE, - '__module__' : 'tcp_node_msgs_pb2' - # @@protoc_insertion_point(class_scope:RecurringTCPResponse) - }) -_sym_db.RegisterMessage(RecurringTCPResponse) +_sym_db.RegisterMessage(SocialPlayerAction) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None - _SERVERDETAILS._serialized_start=23 - _SERVERDETAILS._serialized_end=88 - _SERVERSTYPE1._serialized_start=90 - _SERVERSTYPE1._serialized_end=173 - _SERVERCONNECTIONDETAILSWRAPPER._serialized_start=175 - _SERVERCONNECTIONDETAILSWRAPPER._serialized_end=264 - _SERVERSTYPE2._serialized_start=266 - _SERVERSTYPE2._serialized_end=352 - _TCPSERVERINFO._serialized_start=355 - _TCPSERVERINFO._serialized_end=483 - _TCPHELLO._serialized_start=485 - _TCPHELLO._serialized_end=514 - _RECURRINGTCPRESPONSE._serialized_start=516 - _RECURRINGTCPRESPONSE._serialized_end=594 + _SOCIALPLAYERACTIONTYPE._serialized_start=308 + _SOCIALPLAYERACTIONTYPE._serialized_end=426 + _FLAGTYPE._serialized_start=428 + _FLAGTYPE._serialized_end=536 + _MESSAGEGROUPTYPE._serialized_start=538 + _MESSAGEGROUPTYPE._serialized_end=634 + _SOCIALPLAYERACTION._serialized_start=24 + _SOCIALPLAYERACTION._serialized_end=306 # @@protoc_insertion_point(module_scope) diff --git a/protobuf/udp-node-msgs.proto b/protobuf/udp-node-msgs.proto index 516b583..3a833d6 100644 --- a/protobuf/udp-node-msgs.proto +++ b/protobuf/udp-node-msgs.proto @@ -1,124 +1,265 @@ syntax = "proto2"; +import "profile.proto"; //enums PlayerType and Sport +import "per-session-info.proto"; //TcpConfig + +enum ZofflineConstants { + RealmID = 1; // hardcoded in ZwiftApp=1 (g_CurrentServerRealmID, 0: never connected; -1: disconnected) +} + +enum WA_TYPE { + WAT_LEAVE = 2; //proto::PlayerLeftWorld + WAT_RELOGIN = 3; //proto::PlayerLeftWorld + WAT_RIDE_ON = 4; //proto::RideOn + WAT_SPA = 5; //proto::SocialPlayerAction (chat message) + WAT_EVENT = 6; //proto::Event + WAT_JOIN_E = 7; //proto::PlayerJoinedEvent + WAT_LEFT_E = 8; //proto::PlayerLeftEvent + WAT_RQ_PROF = 9; //proto::RequestProfileFromServer + WAT_INV_W = 10; //proto::ReceiveInvitationWorldAttribute + WAT_KICKED = 11; //no payload + WAT_WTIME = 100; //g_WorldTime := payload (GOD message, may be ignored if ZWIFT\CONFIG\IGNOREGODMESSAGES=1) + WAT_RTIME = 101; //BlimpEntity::SetRoadTime(payload), also GOD message + WAT_B_ACT = 102; //BikeEntity::PerformAction(payload) + WAT_GRP_M = 103; //GroupMessage (may be ignored if ZWIFT\CONFIG\SHOWGROUPMSGS=1) + WAT_PRI_M = 104; //PrivateMessage + WAT_SR = 105; //proto::SegmentResult + WAT_FLAG = 106; //Leaderboards::FlagSandbagger / Leaderboards::FlagCheater + WAT_NONE = 107; //does nothing + WAT_RLA = 108; //ZNETWORK_BroadcastRideLeaderAction + WAT_GE = 109; //GroupEvents::UserSignedup / GroupEvents::UserRegistered + WAT_NM = 110; //notable moment + Play_Magic_Whoosh_Deep_Sparkle + WAT_LATE = 111; //ZNETWORK_INTERNAL_HandleLateJoinRequest + WAT_RH = 112; //ZNETWORK_INTERNAL_HandleRouteHashRequest + WAT_STATS = 113; //GLOBAL_MESSAGE_TYPE_RIDER_FENCE_STATS + WAT_FENCE = 114; //GLOBAL_MESSAGE_TYPE_GRFENCE_CONFIG + WAT_BN_GE = 115; //ZNETWORK_BroadcastBibNumberForGroupEvent + WAT_PPI = 116; //ZNETWORK_INTERNAL_HandlePacePartnerInfo +} + +message WorldAttribute { + optional int64 wa_f1 = 1; //not r/w by game? 587645624533328784, later 5876456 85771834256 + optional int64 server_realm = 2; + optional WA_TYPE wa_type = 3; + optional bytes payload = 4; //not only protobuf + optional int64 world_time_born = 5; + optional int64 x = 6; //stored as int32 + optional int64 y_altitude = 7; //stored as int32 + optional int64 z = 8; //stored as int32 + optional int64 world_time_expire = 9; + optional int64 rel_id = 10; //WAT_PPI: pace partner smth; WAT_SPA: to_player_id; WAT_RH: route_id? ... + optional int32 importance = 11; //not read by game??? WAT_B_ACT:1000; WAT_NM:50000; WAT_RH:5000000; ... 75000 ? + optional int64 wa_f12 = 12; //not r/w by game? Not in package when testing + optional int32 wa_f13 = 13; //not r/w by game? + optional int64 timestamp = 14; //not written by game? (from server) looks like "The Current Epoch Unix Timestamp" in Microseconds + optional int32 wa_f15 = 15; //not r/w by game? 6, might be course + optional int64 wa_f16 = 16; //not r/w by game? stored as bool +} + +/*message WorldAttributes { + repeated WorldAttribute world_attributes = 1; + required int64 world_time = 2; +} + +message World { //zwift.protobuf.World + required uint64 id = 1; + required string name = 2; + required uint64 w_f3 = 3; + optional bool w_f4 = 4; + required uint64 w_f5 = 5; + required uint64 world_time = 6; + required uint64 real_time = 7; + repeated Player w_f8 = 8; +} + +message Player { + optional PlayerProfile player_profile = 1; + optional PlayerState player_state = 2; +}*/ + message PlayerState { - optional int32 id = 1; - optional int64 worldTime = 2; - optional int32 distance = 3; - required int32 roadTime = 4; + optional int64 id = 1; + optional int64 worldTime = 2; // milliseconds + optional int32 distance = 3; // meters + optional int32 roadTime = 4; // 1/100 sec optional int32 laps = 5; - optional int32 speed = 6; + optional uint32 speed = 6; // millimeters per hour + optional uint32 ps_f7 = 7; optional int32 roadPosition = 8; - optional int32 cadenceUHz = 9; + optional int32 cadenceUHz = 9; // =(cad / 60) * 1000000 + optional int32 ps_f10 = 10; // BikeEntity.field_B58; 0 optional int32 heartrate = 11; optional int32 power = 12; optional int64 heading = 13; - optional int32 lean = 14; - optional int32 climbing = 15; - optional int32 time = 16; - optional int32 f19 = 19; - optional int32 f20 = 20; - optional int32 progress = 21; - optional int64 customisationId = 22; - optional int32 justWatching = 23; + optional int64 lean = 14; + optional int32 climbing = 15; // meters + optional int32 time = 16; // seconds + optional int32 ps_f17 = 17; + optional uint32 frameHue = 18; // BikeEntity::DrawBike m_frameHue * 255.0 + //field 19: + //byte[0].bits[0,1]: HasPowerMeter, HasPhoneConnected + //byte[0].bits[2,3]: RoadDirectionForward, ??? !BikeEntity.field_DCC || BikeEntity.disSteer + //byte[0].bits[4]: read in BikeEntity::ProcessNewPacket, steering-related + //byte[1]: =0 ??? + //byte[2]: fallback course/getMapRevisionId + //byte[3]: realRideons (not counted yet in BikeEntity::m_rideons) @ BikeEntity::UpdateRideOns, see also BikeEntity::Update + optional uint32 f19 = 19; + optional uint32 f20 = 20; //road_id: (f20 & 0xff00) >> 8 (=16777231 -> road_id = 0) + optional uint32 progress = 21; // WorkoutMode = progress & 0xF + optional int64 customizationId = 22; + optional bool justWatching = 23; optional int32 calories = 24; optional float x = 25; - optional float altitude = 26; - optional float y = 27; - optional int32 watchingRiderId = 28; - optional int32 groupId = 29; - optional int64 sport = 31; - optional float f34 = 34; + optional float y_altitude = 26; + optional float z = 27; + optional int64 watchingRiderId = 28; + optional int64 groupId = 29; + // 30 absent at least in Android Game + optional Sport sport = 31; + optional float ps_f32 = 32; + optional uint32 ps_f33 = 33; + optional float ps_f34 = 34; //= BikeEntity.field_F00 (=219.56387 and incr if moving) optional int32 world = 35; - optional int32 f38 = 38; - optional uint64 route = 39; + optional uint32 ps_f36 = 36; // = f(BikeEntity.field_2a28) BikeEntity::CreateNewPacket + optional uint32 ps_f37 = 37; // = f(BikeEntity.field_2a28) BikeEntity::CreateNewPacket + optional bool canSteer = 38; // = BikeEntity.m_canSteer + optional int32 route = 39; } message ClientToServer { - required int32 connected = 1; - required int32 player_id = 2; - required int64 world_time = 3; - required int32 seqno = 4; + required int64 server_realm = 1; //UdpClient::sendDisconnectedClientToServer: -1. Otherwise g_CurrentServerRealmID (RealmID or 0 if not connected yet) + required int64 player_id = 2; + optional int64 world_time = 3; + optional uint32 seqno = 4; + optional uint32 cts_f5 = 5; + optional int64 cts_f6 = 6; required PlayerState state = 7; - required int64 f8 = 8; - required int64 f9 = 9; + optional bool cts_f8 = 8; + optional bool cts_f9 = 9; required int64 last_update = 10; - required int64 f11 = 11; + optional bool cts_f11 = 11; required int64 last_player_update = 12; + optional int64 larg_wa_time = 13; //TcpClient::sayHello: LargestWorldAttributeTimestamp + optional bool cts_f14 = 14; + repeated int64 subsSegments = 15; //subscribed segment ids? TcpClient::sayHello, TcpClient::sendSubscribeToSegment + repeated int64 unsSegments = 16; //unsubscribed segment ids? TcpClient::processSegmentUnsubscription } +message PlayerSummary { + optional int32 plsu_f1 = 1; + optional int32 plsu_f2 = 2; + optional int32 plsu_f3 = 3; + optional int32 plsu_f4 = 4; +} + +message PlayerSummaries { + optional sint64 plsus_f1 = 1; //stored as int32 + optional sint64 plsus_f2 = 2; //stored as int32 + optional sint32 plsus_f3 = 3; + optional sint32 plsus_f4 = 4; + optional int32 plsus_f5 = 5; + optional int32 plsus_f6 = 6; + optional int32 plsus_f7 = 7; + repeated PlayerSummary player_summaries = 8; +} + +message RelayAddress { + optional int32 lb_realm = 1; // load balancing cluster: server realm or 0 (generic) + optional int32 lb_course = 2; // load balancing cluster: course id + optional string ip = 3; + optional int32 port = 4; + optional float ra_f5 = 5; //or fixed + optional float ra_f6 = 6; //or fixed +} + +message UdpConfig { + repeated RelayAddress relay_addresses = 1; + optional int32 uc_f2 = 2; //=10? + optional int32 uc_f3 = 3; //=30? + optional int32 uc_f4 = 4; //=3? +} + +message RelayAddressesVOD { + optional int32 lb_realm = 1; // load balancing cluster: server realm or 0 (generic) + optional int32 lb_course = 2; // load balancing cluster: course id + repeated RelayAddress relay_addresses = 3; + optional bool rav_f4 = 4; +} + +message UdpConfigVOD { + repeated RelayAddressesVOD relay_addresses_vod = 1; + optional int32 port = 2; + optional int64 ucv_f3 = 3; + optional int64 ucv_f4 = 4; + optional float ucv_f5 = 5; //or fixed + optional float ucv_f6 = 6; //or fixed +} + +message PlayerRouteDistance { + optional int32 bikeId = 1; //BikeManager::FindBikeWithNetworkID + optional float prd_f2 = 2; //or fixed + optional int32 prd_f3 = 3; //-> m_bikeEntity->field_9C8 +} + +message EventSubgroupPlacements { + optional int32 esp_f1 = 1; //-> m_bikeEntity->field_9C0; UdpStatistics::registerFanViewLatestPlayerStateInfo + repeated PlayerRouteDistance player_rd1 = 2; + repeated PlayerRouteDistance player_rd2 = 3; + repeated PlayerRouteDistance player_rd3 = 4; + repeated PlayerRouteDistance player_rd4 = 5; + optional int32 eventTotalRiders = 6; + optional int32 bikeNetworkId = 7; + optional int32 esp_f8 = 8; //-> BikeWithNetworkID->field_9C8 + optional float esp_f9 = 9; //or fixed +} +enum IPProtocol { + UDP = 1; + TCP = 2; +} message ServerToClient { - required int32 f1 = 1; - required int32 player_id = 2; - required int64 world_time = 3; + optional int64 server_realm = 1; + optional int64 player_id = 2; + optional int64 world_time = 3; optional int32 seqno = 4; - optional int32 f5 = 5; + optional int32 stc_f5 = 5; //read in WorldClockService::calculateOneLegLatency + // 6,7: absent repeated PlayerState states = 8; - repeated PlayerUpdate updates = 9; - optional int64 f11 = 11; + repeated WorldAttribute updates = 9; + repeated int64 stc_f10 = 10; + optional bool stc_f11 = 11; //=true??? + optional string zc_local_ip = 12; + optional int64 stc_f13 = 13; + optional int32 zwifters = 14; + optional int32 zc_local_port = 15; + optional IPProtocol zc_protocol = 16; //TODO: enum; 2: TCP + optional int64 stc_f17 = 17; //read in WorldClockService::calculateOneLegLatency optional int32 num_msgs = 18; optional int32 msgnum = 19; + optional bool hasSimultLogin = 20; //UdpClient::disconnectionRequested due to simultaneous login (1); OR simultaneous login ceased (0) + optional PlayerSummaries player_summaries = 21; //tag426 + // 22 absent + optional EventSubgroupPlacements ev_subgroup_ps = 23; //tag442 + optional UdpConfig udp_config = 24; //tag450 + optional UdpConfigVOD udp_config_vod_1 = 25; //tag458 + optional int32 stc_f26 = 26; //tag464 UdpClient::receivedExpungeReason + optional UdpConfigVOD udp_config_vod_2 = 27; //tag474 + repeated PlayerState player_states = 28; //tag482 + optional TcpConfig tcp_config = 29; //tag490 + repeated int64 ackSubsSegm = 30; //tag496 TcpClient::processSubscribedSegment } -message Ghost { +message Ghost { //not from the Zwift game, zoffline-specific! required int32 player_id = 1; repeated PlayerState states = 2; } -message Ghosts { +message Ghosts { //not from the Zwift game, zoffline-specific! repeated Ghost ghosts = 1; } -message PlayerUpdate { - optional int64 f1 = 1; // 587645624533328784, later 5876456 85771834256 - optional int32 f2 = 2; // 1 - required int32 type = 3; // 105 entered world, 5 chat message, 4 ride on - required bytes payload = 4; // protobuf - optional int64 world_time1 = 5; - optional int64 x = 6; - optional int64 altitude = 7; - optional int64 y = 8; - optional int64 world_time2 = 9; - optional int64 f11 = 11; // 75000 ? - optional int64 f12 = 12; //Not in package when testing - optional int64 f14 = 14; // '1604516817408239', later '1604516824709874' - optional int64 f15 = 15; //6, might be course -} - -message ChatMessage { - required int32 rider_id = 1; - required int32 to_rider_id = 2; // 0 if public message - required int32 f3 = 3; // always value 1 ? - required string firstName = 4; - required string lastName = 5; - required string message = 6; - optional string avatar = 7; - required int32 countryCode = 8; - optional int32 eventSubgroup = 11; -} - message RideOn { - required int32 rider_id = 1; - required int32 to_rider_id = 2; + required int64 player_id = 1; + required int64 to_player_id = 2; required string firstName = 3; required string lastName = 4; required int32 countryCode = 5; } - -message SegmentComplete { - optional int64 f1 = 1; - required int32 rider_id = 2; - optional int32 f3 = 3; - optional int64 f4 = 4; - optional int64 segment_id = 5; - optional int64 f6 = 6; - optional string first_name = 7; - optional string last_name = 8; - optional int64 world_time = 9; - optional int64 milliseconds = 11; - optional int32 f12 = 12; - optional int32 weight_in_grams = 13; - optional int32 f14 = 14; - optional int32 avg_power = 15; - optional int32 f16 = 16; - optional string f7date = 17; - optional int32 f19 = 19; -} \ No newline at end of file diff --git a/protobuf/udp_node_msgs_pb2.py b/protobuf/udp_node_msgs_pb2.py index 425281e..ef14174 100644 --- a/protobuf/udp_node_msgs_pb2.py +++ b/protobuf/udp_node_msgs_pb2.py @@ -2,6 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # source: udp-node-msgs.proto """Generated protocol buffer code.""" +from google.protobuf.internal import enum_type_wrapper from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import message as _message @@ -12,21 +13,72 @@ from google.protobuf import symbol_database as _symbol_database _sym_db = _symbol_database.Default() +import profile_pb2 as profile__pb2 +import per_session_info_pb2 as per__session__info__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x13udp-node-msgs.proto\"\xfe\x03\n\x0bPlayerState\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x11\n\tworldTime\x18\x02 \x01(\x03\x12\x10\n\x08\x64istance\x18\x03 \x01(\x05\x12\x10\n\x08roadTime\x18\x04 \x02(\x05\x12\x0c\n\x04laps\x18\x05 \x01(\x05\x12\r\n\x05speed\x18\x06 \x01(\x05\x12\x14\n\x0croadPosition\x18\x08 \x01(\x05\x12\x12\n\ncadenceUHz\x18\t \x01(\x05\x12\x11\n\theartrate\x18\x0b \x01(\x05\x12\r\n\x05power\x18\x0c \x01(\x05\x12\x0f\n\x07heading\x18\r \x01(\x03\x12\x0c\n\x04lean\x18\x0e \x01(\x05\x12\x10\n\x08\x63limbing\x18\x0f \x01(\x05\x12\x0c\n\x04time\x18\x10 \x01(\x05\x12\x0b\n\x03\x66\x31\x39\x18\x13 \x01(\x05\x12\x0b\n\x03\x66\x32\x30\x18\x14 \x01(\x05\x12\x10\n\x08progress\x18\x15 \x01(\x05\x12\x17\n\x0f\x63ustomisationId\x18\x16 \x01(\x03\x12\x14\n\x0cjustWatching\x18\x17 \x01(\x05\x12\x10\n\x08\x63\x61lories\x18\x18 \x01(\x05\x12\t\n\x01x\x18\x19 \x01(\x02\x12\x10\n\x08\x61ltitude\x18\x1a \x01(\x02\x12\t\n\x01y\x18\x1b \x01(\x02\x12\x17\n\x0fwatchingRiderId\x18\x1c \x01(\x05\x12\x0f\n\x07groupId\x18\x1d \x01(\x05\x12\r\n\x05sport\x18\x1f \x01(\x03\x12\x0b\n\x03\x66\x33\x34\x18\" \x01(\x02\x12\r\n\x05world\x18# \x01(\x05\x12\x0b\n\x03\x66\x33\x38\x18& \x01(\x05\x12\r\n\x05route\x18\' \x01(\x04\"\xcc\x01\n\x0e\x43lientToServer\x12\x11\n\tconnected\x18\x01 \x02(\x05\x12\x11\n\tplayer_id\x18\x02 \x02(\x05\x12\x12\n\nworld_time\x18\x03 \x02(\x03\x12\r\n\x05seqno\x18\x04 \x02(\x05\x12\x1b\n\x05state\x18\x07 \x02(\x0b\x32\x0c.PlayerState\x12\n\n\x02\x66\x38\x18\x08 \x02(\x03\x12\n\n\x02\x66\x39\x18\t \x02(\x03\x12\x13\n\x0blast_update\x18\n \x02(\x03\x12\x0b\n\x03\x66\x31\x31\x18\x0b \x02(\x03\x12\x1a\n\x12last_player_update\x18\x0c \x02(\x03\"\xcb\x01\n\x0eServerToClient\x12\n\n\x02\x66\x31\x18\x01 \x02(\x05\x12\x11\n\tplayer_id\x18\x02 \x02(\x05\x12\x12\n\nworld_time\x18\x03 \x02(\x03\x12\r\n\x05seqno\x18\x04 \x01(\x05\x12\n\n\x02\x66\x35\x18\x05 \x01(\x05\x12\x1c\n\x06states\x18\x08 \x03(\x0b\x32\x0c.PlayerState\x12\x1e\n\x07updates\x18\t \x03(\x0b\x32\r.PlayerUpdate\x12\x0b\n\x03\x66\x31\x31\x18\x0b \x01(\x03\x12\x10\n\x08num_msgs\x18\x12 \x01(\x05\x12\x0e\n\x06msgnum\x18\x13 \x01(\x05\"8\n\x05Ghost\x12\x11\n\tplayer_id\x18\x01 \x02(\x05\x12\x1c\n\x06states\x18\x02 \x03(\x0b\x32\x0c.PlayerState\" \n\x06Ghosts\x12\x16\n\x06ghosts\x18\x01 \x03(\x0b\x32\x06.Ghost\"\xcb\x01\n\x0cPlayerUpdate\x12\n\n\x02\x66\x31\x18\x01 \x01(\x03\x12\n\n\x02\x66\x32\x18\x02 \x01(\x05\x12\x0c\n\x04type\x18\x03 \x02(\x05\x12\x0f\n\x07payload\x18\x04 \x02(\x0c\x12\x13\n\x0bworld_time1\x18\x05 \x01(\x03\x12\t\n\x01x\x18\x06 \x01(\x03\x12\x10\n\x08\x61ltitude\x18\x07 \x01(\x03\x12\t\n\x01y\x18\x08 \x01(\x03\x12\x13\n\x0bworld_time2\x18\t \x01(\x03\x12\x0b\n\x03\x66\x31\x31\x18\x0b \x01(\x03\x12\x0b\n\x03\x66\x31\x32\x18\x0c \x01(\x03\x12\x0b\n\x03\x66\x31\x34\x18\x0e \x01(\x03\x12\x0b\n\x03\x66\x31\x35\x18\x0f \x01(\x03\"\xb2\x01\n\x0b\x43hatMessage\x12\x10\n\x08rider_id\x18\x01 \x02(\x05\x12\x13\n\x0bto_rider_id\x18\x02 \x02(\x05\x12\n\n\x02\x66\x33\x18\x03 \x02(\x05\x12\x11\n\tfirstName\x18\x04 \x02(\t\x12\x10\n\x08lastName\x18\x05 \x02(\t\x12\x0f\n\x07message\x18\x06 \x02(\t\x12\x0e\n\x06\x61vatar\x18\x07 \x01(\t\x12\x13\n\x0b\x63ountryCode\x18\x08 \x02(\x05\x12\x15\n\reventSubgroup\x18\x0b \x01(\x05\"i\n\x06RideOn\x12\x10\n\x08rider_id\x18\x01 \x02(\x05\x12\x13\n\x0bto_rider_id\x18\x02 \x02(\x05\x12\x11\n\tfirstName\x18\x03 \x02(\t\x12\x10\n\x08lastName\x18\x04 \x02(\t\x12\x13\n\x0b\x63ountryCode\x18\x05 \x02(\x05\"\xa8\x02\n\x0fSegmentComplete\x12\n\n\x02\x66\x31\x18\x01 \x01(\x03\x12\x10\n\x08rider_id\x18\x02 \x02(\x05\x12\n\n\x02\x66\x33\x18\x03 \x01(\x05\x12\n\n\x02\x66\x34\x18\x04 \x01(\x03\x12\x12\n\nsegment_id\x18\x05 \x01(\x03\x12\n\n\x02\x66\x36\x18\x06 \x01(\x03\x12\x12\n\nfirst_name\x18\x07 \x01(\t\x12\x11\n\tlast_name\x18\x08 \x01(\t\x12\x12\n\nworld_time\x18\t \x01(\x03\x12\x14\n\x0cmilliseconds\x18\x0b \x01(\x03\x12\x0b\n\x03\x66\x31\x32\x18\x0c \x01(\x05\x12\x17\n\x0fweight_in_grams\x18\r \x01(\x05\x12\x0b\n\x03\x66\x31\x34\x18\x0e \x01(\x05\x12\x11\n\tavg_power\x18\x0f \x01(\x05\x12\x0b\n\x03\x66\x31\x36\x18\x10 \x01(\x05\x12\x0e\n\x06\x66\x37\x64\x61te\x18\x11 \x01(\t\x12\x0b\n\x03\x66\x31\x39\x18\x13 \x01(\x05') - +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x13udp-node-msgs.proto\x1a\rprofile.proto\x1a\x16per-session-info.proto\"\xb6\x02\n\x0eWorldAttribute\x12\r\n\x05wa_f1\x18\x01 \x01(\x03\x12\x14\n\x0cserver_realm\x18\x02 \x01(\x03\x12\x19\n\x07wa_type\x18\x03 \x01(\x0e\x32\x08.WA_TYPE\x12\x0f\n\x07payload\x18\x04 \x01(\x0c\x12\x17\n\x0fworld_time_born\x18\x05 \x01(\x03\x12\t\n\x01x\x18\x06 \x01(\x03\x12\x12\n\ny_altitude\x18\x07 \x01(\x03\x12\t\n\x01z\x18\x08 \x01(\x03\x12\x19\n\x11world_time_expire\x18\t \x01(\x03\x12\x0e\n\x06rel_id\x18\n \x01(\x03\x12\x12\n\nimportance\x18\x0b \x01(\x05\x12\x0e\n\x06wa_f12\x18\x0c \x01(\x03\x12\x0e\n\x06wa_f13\x18\r \x01(\x05\x12\x11\n\ttimestamp\x18\x0e \x01(\x03\x12\x0e\n\x06wa_f15\x18\x0f \x01(\x05\x12\x0e\n\x06wa_f16\x18\x10 \x01(\x03\"\x91\x05\n\x0bPlayerState\x12\n\n\x02id\x18\x01 \x01(\x03\x12\x11\n\tworldTime\x18\x02 \x01(\x03\x12\x10\n\x08\x64istance\x18\x03 \x01(\x05\x12\x10\n\x08roadTime\x18\x04 \x01(\x05\x12\x0c\n\x04laps\x18\x05 \x01(\x05\x12\r\n\x05speed\x18\x06 \x01(\r\x12\r\n\x05ps_f7\x18\x07 \x01(\r\x12\x14\n\x0croadPosition\x18\x08 \x01(\x05\x12\x12\n\ncadenceUHz\x18\t \x01(\x05\x12\x0e\n\x06ps_f10\x18\n \x01(\x05\x12\x11\n\theartrate\x18\x0b \x01(\x05\x12\r\n\x05power\x18\x0c \x01(\x05\x12\x0f\n\x07heading\x18\r \x01(\x03\x12\x0c\n\x04lean\x18\x0e \x01(\x03\x12\x10\n\x08\x63limbing\x18\x0f \x01(\x05\x12\x0c\n\x04time\x18\x10 \x01(\x05\x12\x0e\n\x06ps_f17\x18\x11 \x01(\x05\x12\x10\n\x08\x66rameHue\x18\x12 \x01(\r\x12\x0b\n\x03\x66\x31\x39\x18\x13 \x01(\r\x12\x0b\n\x03\x66\x32\x30\x18\x14 \x01(\r\x12\x10\n\x08progress\x18\x15 \x01(\r\x12\x17\n\x0f\x63ustomizationId\x18\x16 \x01(\x03\x12\x14\n\x0cjustWatching\x18\x17 \x01(\x08\x12\x10\n\x08\x63\x61lories\x18\x18 \x01(\x05\x12\t\n\x01x\x18\x19 \x01(\x02\x12\x12\n\ny_altitude\x18\x1a \x01(\x02\x12\t\n\x01z\x18\x1b \x01(\x02\x12\x17\n\x0fwatchingRiderId\x18\x1c \x01(\x03\x12\x0f\n\x07groupId\x18\x1d \x01(\x03\x12\x15\n\x05sport\x18\x1f \x01(\x0e\x32\x06.Sport\x12\x0e\n\x06ps_f32\x18 \x01(\x02\x12\x0e\n\x06ps_f33\x18! \x01(\r\x12\x0e\n\x06ps_f34\x18\" \x01(\x02\x12\r\n\x05world\x18# \x01(\x05\x12\x0e\n\x06ps_f36\x18$ \x01(\r\x12\x0e\n\x06ps_f37\x18% \x01(\r\x12\x10\n\x08\x63\x61nSteer\x18& \x01(\x08\x12\r\n\x05route\x18\' \x01(\x05\"\xcd\x02\n\x0e\x43lientToServer\x12\x14\n\x0cserver_realm\x18\x01 \x02(\x03\x12\x11\n\tplayer_id\x18\x02 \x02(\x03\x12\x12\n\nworld_time\x18\x03 \x01(\x03\x12\r\n\x05seqno\x18\x04 \x01(\r\x12\x0e\n\x06\x63ts_f5\x18\x05 \x01(\r\x12\x0e\n\x06\x63ts_f6\x18\x06 \x01(\x03\x12\x1b\n\x05state\x18\x07 \x02(\x0b\x32\x0c.PlayerState\x12\x0e\n\x06\x63ts_f8\x18\x08 \x01(\x08\x12\x0e\n\x06\x63ts_f9\x18\t \x01(\x08\x12\x13\n\x0blast_update\x18\n \x02(\x03\x12\x0f\n\x07\x63ts_f11\x18\x0b \x01(\x08\x12\x1a\n\x12last_player_update\x18\x0c \x02(\x03\x12\x14\n\x0clarg_wa_time\x18\r \x01(\x03\x12\x0f\n\x07\x63ts_f14\x18\x0e \x01(\x08\x12\x14\n\x0csubsSegments\x18\x0f \x03(\x03\x12\x13\n\x0bunsSegments\x18\x10 \x03(\x03\"S\n\rPlayerSummary\x12\x0f\n\x07plsu_f1\x18\x01 \x01(\x05\x12\x0f\n\x07plsu_f2\x18\x02 \x01(\x05\x12\x0f\n\x07plsu_f3\x18\x03 \x01(\x05\x12\x0f\n\x07plsu_f4\x18\x04 \x01(\x05\"\xb9\x01\n\x0fPlayerSummaries\x12\x10\n\x08plsus_f1\x18\x01 \x01(\x12\x12\x10\n\x08plsus_f2\x18\x02 \x01(\x12\x12\x10\n\x08plsus_f3\x18\x03 \x01(\x11\x12\x10\n\x08plsus_f4\x18\x04 \x01(\x11\x12\x10\n\x08plsus_f5\x18\x05 \x01(\x05\x12\x10\n\x08plsus_f6\x18\x06 \x01(\x05\x12\x10\n\x08plsus_f7\x18\x07 \x01(\x05\x12(\n\x10player_summaries\x18\x08 \x03(\x0b\x32\x0e.PlayerSummary\"k\n\x0cRelayAddress\x12\x10\n\x08lb_realm\x18\x01 \x01(\x05\x12\x11\n\tlb_course\x18\x02 \x01(\x05\x12\n\n\x02ip\x18\x03 \x01(\t\x12\x0c\n\x04port\x18\x04 \x01(\x05\x12\r\n\x05ra_f5\x18\x05 \x01(\x02\x12\r\n\x05ra_f6\x18\x06 \x01(\x02\"`\n\tUdpConfig\x12&\n\x0frelay_addresses\x18\x01 \x03(\x0b\x32\r.RelayAddress\x12\r\n\x05uc_f2\x18\x02 \x01(\x05\x12\r\n\x05uc_f3\x18\x03 \x01(\x05\x12\r\n\x05uc_f4\x18\x04 \x01(\x05\"p\n\x11RelayAddressesVOD\x12\x10\n\x08lb_realm\x18\x01 \x01(\x05\x12\x11\n\tlb_course\x18\x02 \x01(\x05\x12&\n\x0frelay_addresses\x18\x03 \x03(\x0b\x32\r.RelayAddress\x12\x0e\n\x06rav_f4\x18\x04 \x01(\x08\"\x8d\x01\n\x0cUdpConfigVOD\x12/\n\x13relay_addresses_vod\x18\x01 \x03(\x0b\x32\x12.RelayAddressesVOD\x12\x0c\n\x04port\x18\x02 \x01(\x05\x12\x0e\n\x06ucv_f3\x18\x03 \x01(\x03\x12\x0e\n\x06ucv_f4\x18\x04 \x01(\x03\x12\x0e\n\x06ucv_f5\x18\x05 \x01(\x02\x12\x0e\n\x06ucv_f6\x18\x06 \x01(\x02\"E\n\x13PlayerRouteDistance\x12\x0e\n\x06\x62ikeId\x18\x01 \x01(\x05\x12\x0e\n\x06prd_f2\x18\x02 \x01(\x02\x12\x0e\n\x06prd_f3\x18\x03 \x01(\x05\"\xa2\x02\n\x17\x45ventSubgroupPlacements\x12\x0e\n\x06\x65sp_f1\x18\x01 \x01(\x05\x12(\n\nplayer_rd1\x18\x02 \x03(\x0b\x32\x14.PlayerRouteDistance\x12(\n\nplayer_rd2\x18\x03 \x03(\x0b\x32\x14.PlayerRouteDistance\x12(\n\nplayer_rd3\x18\x04 \x03(\x0b\x32\x14.PlayerRouteDistance\x12(\n\nplayer_rd4\x18\x05 \x03(\x0b\x32\x14.PlayerRouteDistance\x12\x18\n\x10\x65ventTotalRiders\x18\x06 \x01(\x05\x12\x15\n\rbikeNetworkId\x18\x07 \x01(\x05\x12\x0e\n\x06\x65sp_f8\x18\x08 \x01(\x05\x12\x0e\n\x06\x65sp_f9\x18\t \x01(\x02\"\xc5\x05\n\x0eServerToClient\x12\x14\n\x0cserver_realm\x18\x01 \x01(\x03\x12\x11\n\tplayer_id\x18\x02 \x01(\x03\x12\x12\n\nworld_time\x18\x03 \x01(\x03\x12\r\n\x05seqno\x18\x04 \x01(\x05\x12\x0e\n\x06stc_f5\x18\x05 \x01(\x05\x12\x1c\n\x06states\x18\x08 \x03(\x0b\x32\x0c.PlayerState\x12 \n\x07updates\x18\t \x03(\x0b\x32\x0f.WorldAttribute\x12\x0f\n\x07stc_f10\x18\n \x03(\x03\x12\x0f\n\x07stc_f11\x18\x0b \x01(\x08\x12\x13\n\x0bzc_local_ip\x18\x0c \x01(\t\x12\x0f\n\x07stc_f13\x18\r \x01(\x03\x12\x10\n\x08zwifters\x18\x0e \x01(\x05\x12\x15\n\rzc_local_port\x18\x0f \x01(\x05\x12 \n\x0bzc_protocol\x18\x10 \x01(\x0e\x32\x0b.IPProtocol\x12\x0f\n\x07stc_f17\x18\x11 \x01(\x03\x12\x10\n\x08num_msgs\x18\x12 \x01(\x05\x12\x0e\n\x06msgnum\x18\x13 \x01(\x05\x12\x16\n\x0ehasSimultLogin\x18\x14 \x01(\x08\x12*\n\x10player_summaries\x18\x15 \x01(\x0b\x32\x10.PlayerSummaries\x12\x30\n\x0e\x65v_subgroup_ps\x18\x17 \x01(\x0b\x32\x18.EventSubgroupPlacements\x12\x1e\n\nudp_config\x18\x18 \x01(\x0b\x32\n.UdpConfig\x12\'\n\x10udp_config_vod_1\x18\x19 \x01(\x0b\x32\r.UdpConfigVOD\x12\x0f\n\x07stc_f26\x18\x1a \x01(\x05\x12\'\n\x10udp_config_vod_2\x18\x1b \x01(\x0b\x32\r.UdpConfigVOD\x12#\n\rplayer_states\x18\x1c \x03(\x0b\x32\x0c.PlayerState\x12\x1e\n\ntcp_config\x18\x1d \x01(\x0b\x32\n.TcpConfig\x12\x13\n\x0b\x61\x63kSubsSegm\x18\x1e \x03(\x03\"8\n\x05Ghost\x12\x11\n\tplayer_id\x18\x01 \x02(\x05\x12\x1c\n\x06states\x18\x02 \x03(\x0b\x32\x0c.PlayerState\" \n\x06Ghosts\x12\x16\n\x06ghosts\x18\x01 \x03(\x0b\x32\x06.Ghost\"k\n\x06RideOn\x12\x11\n\tplayer_id\x18\x01 \x02(\x03\x12\x14\n\x0cto_player_id\x18\x02 \x02(\x03\x12\x11\n\tfirstName\x18\x03 \x02(\t\x12\x10\n\x08lastName\x18\x04 \x02(\t\x12\x13\n\x0b\x63ountryCode\x18\x05 \x02(\x05* \n\x11ZofflineConstants\x12\x0b\n\x07RealmID\x10\x01*\x92\x03\n\x07WA_TYPE\x12\r\n\tWAT_LEAVE\x10\x02\x12\x0f\n\x0bWAT_RELOGIN\x10\x03\x12\x0f\n\x0bWAT_RIDE_ON\x10\x04\x12\x0b\n\x07WAT_SPA\x10\x05\x12\r\n\tWAT_EVENT\x10\x06\x12\x0e\n\nWAT_JOIN_E\x10\x07\x12\x0e\n\nWAT_LEFT_E\x10\x08\x12\x0f\n\x0bWAT_RQ_PROF\x10\t\x12\r\n\tWAT_INV_W\x10\n\x12\x0e\n\nWAT_KICKED\x10\x0b\x12\r\n\tWAT_WTIME\x10\x64\x12\r\n\tWAT_RTIME\x10\x65\x12\r\n\tWAT_B_ACT\x10\x66\x12\r\n\tWAT_GRP_M\x10g\x12\r\n\tWAT_PRI_M\x10h\x12\n\n\x06WAT_SR\x10i\x12\x0c\n\x08WAT_FLAG\x10j\x12\x0c\n\x08WAT_NONE\x10k\x12\x0b\n\x07WAT_RLA\x10l\x12\n\n\x06WAT_GE\x10m\x12\n\n\x06WAT_NM\x10n\x12\x0c\n\x08WAT_LATE\x10o\x12\n\n\x06WAT_RH\x10p\x12\r\n\tWAT_STATS\x10q\x12\r\n\tWAT_FENCE\x10r\x12\r\n\tWAT_BN_GE\x10s\x12\x0b\n\x07WAT_PPI\x10t*\x1e\n\nIPProtocol\x12\x07\n\x03UDP\x10\x01\x12\x07\n\x03TCP\x10\x02') + +_ZOFFLINECONSTANTS = DESCRIPTOR.enum_types_by_name['ZofflineConstants'] +ZofflineConstants = enum_type_wrapper.EnumTypeWrapper(_ZOFFLINECONSTANTS) +_WA_TYPE = DESCRIPTOR.enum_types_by_name['WA_TYPE'] +WA_TYPE = enum_type_wrapper.EnumTypeWrapper(_WA_TYPE) +_IPPROTOCOL = DESCRIPTOR.enum_types_by_name['IPProtocol'] +IPProtocol = enum_type_wrapper.EnumTypeWrapper(_IPPROTOCOL) +RealmID = 1 +WAT_LEAVE = 2 +WAT_RELOGIN = 3 +WAT_RIDE_ON = 4 +WAT_SPA = 5 +WAT_EVENT = 6 +WAT_JOIN_E = 7 +WAT_LEFT_E = 8 +WAT_RQ_PROF = 9 +WAT_INV_W = 10 +WAT_KICKED = 11 +WAT_WTIME = 100 +WAT_RTIME = 101 +WAT_B_ACT = 102 +WAT_GRP_M = 103 +WAT_PRI_M = 104 +WAT_SR = 105 +WAT_FLAG = 106 +WAT_NONE = 107 +WAT_RLA = 108 +WAT_GE = 109 +WAT_NM = 110 +WAT_LATE = 111 +WAT_RH = 112 +WAT_STATS = 113 +WAT_FENCE = 114 +WAT_BN_GE = 115 +WAT_PPI = 116 +UDP = 1 +TCP = 2 +_WORLDATTRIBUTE = DESCRIPTOR.message_types_by_name['WorldAttribute'] _PLAYERSTATE = DESCRIPTOR.message_types_by_name['PlayerState'] _CLIENTTOSERVER = DESCRIPTOR.message_types_by_name['ClientToServer'] +_PLAYERSUMMARY = DESCRIPTOR.message_types_by_name['PlayerSummary'] +_PLAYERSUMMARIES = DESCRIPTOR.message_types_by_name['PlayerSummaries'] +_RELAYADDRESS = DESCRIPTOR.message_types_by_name['RelayAddress'] +_UDPCONFIG = DESCRIPTOR.message_types_by_name['UdpConfig'] +_RELAYADDRESSESVOD = DESCRIPTOR.message_types_by_name['RelayAddressesVOD'] +_UDPCONFIGVOD = DESCRIPTOR.message_types_by_name['UdpConfigVOD'] +_PLAYERROUTEDISTANCE = DESCRIPTOR.message_types_by_name['PlayerRouteDistance'] +_EVENTSUBGROUPPLACEMENTS = DESCRIPTOR.message_types_by_name['EventSubgroupPlacements'] _SERVERTOCLIENT = DESCRIPTOR.message_types_by_name['ServerToClient'] _GHOST = DESCRIPTOR.message_types_by_name['Ghost'] _GHOSTS = DESCRIPTOR.message_types_by_name['Ghosts'] -_PLAYERUPDATE = DESCRIPTOR.message_types_by_name['PlayerUpdate'] -_CHATMESSAGE = DESCRIPTOR.message_types_by_name['ChatMessage'] _RIDEON = DESCRIPTOR.message_types_by_name['RideOn'] -_SEGMENTCOMPLETE = DESCRIPTOR.message_types_by_name['SegmentComplete'] +WorldAttribute = _reflection.GeneratedProtocolMessageType('WorldAttribute', (_message.Message,), { + 'DESCRIPTOR' : _WORLDATTRIBUTE, + '__module__' : 'udp_node_msgs_pb2' + # @@protoc_insertion_point(class_scope:WorldAttribute) + }) +_sym_db.RegisterMessage(WorldAttribute) + PlayerState = _reflection.GeneratedProtocolMessageType('PlayerState', (_message.Message,), { 'DESCRIPTOR' : _PLAYERSTATE, '__module__' : 'udp_node_msgs_pb2' @@ -41,6 +93,62 @@ ClientToServer = _reflection.GeneratedProtocolMessageType('ClientToServer', (_me }) _sym_db.RegisterMessage(ClientToServer) +PlayerSummary = _reflection.GeneratedProtocolMessageType('PlayerSummary', (_message.Message,), { + 'DESCRIPTOR' : _PLAYERSUMMARY, + '__module__' : 'udp_node_msgs_pb2' + # @@protoc_insertion_point(class_scope:PlayerSummary) + }) +_sym_db.RegisterMessage(PlayerSummary) + +PlayerSummaries = _reflection.GeneratedProtocolMessageType('PlayerSummaries', (_message.Message,), { + 'DESCRIPTOR' : _PLAYERSUMMARIES, + '__module__' : 'udp_node_msgs_pb2' + # @@protoc_insertion_point(class_scope:PlayerSummaries) + }) +_sym_db.RegisterMessage(PlayerSummaries) + +RelayAddress = _reflection.GeneratedProtocolMessageType('RelayAddress', (_message.Message,), { + 'DESCRIPTOR' : _RELAYADDRESS, + '__module__' : 'udp_node_msgs_pb2' + # @@protoc_insertion_point(class_scope:RelayAddress) + }) +_sym_db.RegisterMessage(RelayAddress) + +UdpConfig = _reflection.GeneratedProtocolMessageType('UdpConfig', (_message.Message,), { + 'DESCRIPTOR' : _UDPCONFIG, + '__module__' : 'udp_node_msgs_pb2' + # @@protoc_insertion_point(class_scope:UdpConfig) + }) +_sym_db.RegisterMessage(UdpConfig) + +RelayAddressesVOD = _reflection.GeneratedProtocolMessageType('RelayAddressesVOD', (_message.Message,), { + 'DESCRIPTOR' : _RELAYADDRESSESVOD, + '__module__' : 'udp_node_msgs_pb2' + # @@protoc_insertion_point(class_scope:RelayAddressesVOD) + }) +_sym_db.RegisterMessage(RelayAddressesVOD) + +UdpConfigVOD = _reflection.GeneratedProtocolMessageType('UdpConfigVOD', (_message.Message,), { + 'DESCRIPTOR' : _UDPCONFIGVOD, + '__module__' : 'udp_node_msgs_pb2' + # @@protoc_insertion_point(class_scope:UdpConfigVOD) + }) +_sym_db.RegisterMessage(UdpConfigVOD) + +PlayerRouteDistance = _reflection.GeneratedProtocolMessageType('PlayerRouteDistance', (_message.Message,), { + 'DESCRIPTOR' : _PLAYERROUTEDISTANCE, + '__module__' : 'udp_node_msgs_pb2' + # @@protoc_insertion_point(class_scope:PlayerRouteDistance) + }) +_sym_db.RegisterMessage(PlayerRouteDistance) + +EventSubgroupPlacements = _reflection.GeneratedProtocolMessageType('EventSubgroupPlacements', (_message.Message,), { + 'DESCRIPTOR' : _EVENTSUBGROUPPLACEMENTS, + '__module__' : 'udp_node_msgs_pb2' + # @@protoc_insertion_point(class_scope:EventSubgroupPlacements) + }) +_sym_db.RegisterMessage(EventSubgroupPlacements) + ServerToClient = _reflection.GeneratedProtocolMessageType('ServerToClient', (_message.Message,), { 'DESCRIPTOR' : _SERVERTOCLIENT, '__module__' : 'udp_node_msgs_pb2' @@ -62,20 +170,6 @@ Ghosts = _reflection.GeneratedProtocolMessageType('Ghosts', (_message.Message,), }) _sym_db.RegisterMessage(Ghosts) -PlayerUpdate = _reflection.GeneratedProtocolMessageType('PlayerUpdate', (_message.Message,), { - 'DESCRIPTOR' : _PLAYERUPDATE, - '__module__' : 'udp_node_msgs_pb2' - # @@protoc_insertion_point(class_scope:PlayerUpdate) - }) -_sym_db.RegisterMessage(PlayerUpdate) - -ChatMessage = _reflection.GeneratedProtocolMessageType('ChatMessage', (_message.Message,), { - 'DESCRIPTOR' : _CHATMESSAGE, - '__module__' : 'udp_node_msgs_pb2' - # @@protoc_insertion_point(class_scope:ChatMessage) - }) -_sym_db.RegisterMessage(ChatMessage) - RideOn = _reflection.GeneratedProtocolMessageType('RideOn', (_message.Message,), { 'DESCRIPTOR' : _RIDEON, '__module__' : 'udp_node_msgs_pb2' @@ -83,32 +177,43 @@ RideOn = _reflection.GeneratedProtocolMessageType('RideOn', (_message.Message,), }) _sym_db.RegisterMessage(RideOn) -SegmentComplete = _reflection.GeneratedProtocolMessageType('SegmentComplete', (_message.Message,), { - 'DESCRIPTOR' : _SEGMENTCOMPLETE, - '__module__' : 'udp_node_msgs_pb2' - # @@protoc_insertion_point(class_scope:SegmentComplete) - }) -_sym_db.RegisterMessage(SegmentComplete) - if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None - _PLAYERSTATE._serialized_start=24 - _PLAYERSTATE._serialized_end=534 - _CLIENTTOSERVER._serialized_start=537 - _CLIENTTOSERVER._serialized_end=741 - _SERVERTOCLIENT._serialized_start=744 - _SERVERTOCLIENT._serialized_end=947 - _GHOST._serialized_start=949 - _GHOST._serialized_end=1005 - _GHOSTS._serialized_start=1007 - _GHOSTS._serialized_end=1039 - _PLAYERUPDATE._serialized_start=1042 - _PLAYERUPDATE._serialized_end=1245 - _CHATMESSAGE._serialized_start=1248 - _CHATMESSAGE._serialized_end=1426 - _RIDEON._serialized_start=1428 - _RIDEON._serialized_end=1533 - _SEGMENTCOMPLETE._serialized_start=1536 - _SEGMENTCOMPLETE._serialized_end=1832 + _ZOFFLINECONSTANTS._serialized_start=3386 + _ZOFFLINECONSTANTS._serialized_end=3418 + _WA_TYPE._serialized_start=3421 + _WA_TYPE._serialized_end=3823 + _IPPROTOCOL._serialized_start=3825 + _IPPROTOCOL._serialized_end=3855 + _WORLDATTRIBUTE._serialized_start=63 + _WORLDATTRIBUTE._serialized_end=373 + _PLAYERSTATE._serialized_start=376 + _PLAYERSTATE._serialized_end=1033 + _CLIENTTOSERVER._serialized_start=1036 + _CLIENTTOSERVER._serialized_end=1369 + _PLAYERSUMMARY._serialized_start=1371 + _PLAYERSUMMARY._serialized_end=1454 + _PLAYERSUMMARIES._serialized_start=1457 + _PLAYERSUMMARIES._serialized_end=1642 + _RELAYADDRESS._serialized_start=1644 + _RELAYADDRESS._serialized_end=1751 + _UDPCONFIG._serialized_start=1753 + _UDPCONFIG._serialized_end=1849 + _RELAYADDRESSESVOD._serialized_start=1851 + _RELAYADDRESSESVOD._serialized_end=1963 + _UDPCONFIGVOD._serialized_start=1966 + _UDPCONFIGVOD._serialized_end=2107 + _PLAYERROUTEDISTANCE._serialized_start=2109 + _PLAYERROUTEDISTANCE._serialized_end=2178 + _EVENTSUBGROUPPLACEMENTS._serialized_start=2181 + _EVENTSUBGROUPPLACEMENTS._serialized_end=2471 + _SERVERTOCLIENT._serialized_start=2474 + _SERVERTOCLIENT._serialized_end=3183 + _GHOST._serialized_start=3185 + _GHOST._serialized_end=3241 + _GHOSTS._serialized_start=3243 + _GHOSTS._serialized_end=3275 + _RIDEON._serialized_start=3277 + _RIDEON._serialized_end=3384 # @@protoc_insertion_point(module_scope) diff --git a/protobuf/world.proto b/protobuf/world.proto index 4761414..855423e 100644 --- a/protobuf/world.proto +++ b/protobuf/world.proto @@ -1,43 +1,43 @@ syntax = "proto2"; -message World { - required uint32 id = 1; - required string name = 2; - required uint32 f3 = 3; - /* missing 4 */ - required uint64 f5 = 5; - required uint64 world_time = 6; - required uint64 real_time = 7; - repeated Player player_states = 8; - repeated Player pace_partner_states = 12; +import "profile.proto"; //enums PlayerType and Sport + +message DropInWorld { + required uint64 id = 1; + optional string name = 2; + optional uint64 course_id = 3; + optional bool f4 = 4; + optional uint64 zwifters = 5; + optional uint64 world_time = 6; + optional uint64 real_time = 7; + repeated DropInPlayer pro_players = 8; + repeated DropInPlayer followees = 9; + repeated DropInPlayer others = 10; + optional uint64 max_zwifters = 11; //stored as int32 + repeated DropInPlayer pacer_bots = 12; } -message Worlds { - repeated World worlds = 1; +message DropInWorldList { + repeated DropInWorld worlds = 1; } -message WorldAttributes { - /* repeated RiderAttributes riders = 1; */ - required int64 world_time = 2; -} - -message Player { - required uint32 id = 1; +message DropInPlayer { + required uint64 id = 1; required string firstName = 2; required string lastName = 3; - optional uint32 distance = 4; - optional uint32 time = 5; - optional uint32 f6 = 6; - optional uint32 f7 = 7; - optional uint32 f8 = 8; - optional uint32 f9 = 9; - optional uint32 f10 = 10; - optional uint32 f11 = 11; - optional uint32 power = 12; - optional uint32 f13 = 13; + optional uint64 distance = 4; + optional uint64 time = 5; + optional uint64 country_code = 6; + optional PlayerType player_type = 7; + optional Sport sport = 8; + optional bool f9 = 9; + optional bool f10 = 10; + optional uint64 f11 = 11; + optional uint64 power = 12; + optional uint64 f13 = 13; optional float x = 14; - optional float altitude = 15; - optional float y = 16; - optional uint64 route = 17; - optional uint32 f18 = 18; - optional uint32 f19 = 19; + optional float y_altitude = 15; + optional float z = 16; + optional int32 route = 17; + optional uint32 ride_power = 18; + optional uint32 speed = 19; } \ No newline at end of file diff --git a/protobuf/world_pb2.py b/protobuf/world_pb2.py index f43b042..5064f4d 100644 --- a/protobuf/world_pb2.py +++ b/protobuf/world_pb2.py @@ -12,53 +12,44 @@ from google.protobuf import symbol_database as _symbol_database _sym_db = _symbol_database.Default() +import profile_pb2 as profile__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0bworld.proto\"\xa6\x01\n\x05World\x12\n\n\x02id\x18\x01 \x02(\r\x12\x0c\n\x04name\x18\x02 \x02(\t\x12\n\n\x02\x66\x33\x18\x03 \x02(\r\x12\n\n\x02\x66\x35\x18\x05 \x02(\x04\x12\x12\n\nworld_time\x18\x06 \x02(\x04\x12\x11\n\treal_time\x18\x07 \x02(\x04\x12\x1e\n\rplayer_states\x18\x08 \x03(\x0b\x32\x07.Player\x12$\n\x13pace_partner_states\x18\x0c \x03(\x0b\x32\x07.Player\" \n\x06Worlds\x12\x16\n\x06worlds\x18\x01 \x03(\x0b\x32\x06.World\"%\n\x0fWorldAttributes\x12\x12\n\nworld_time\x18\x02 \x02(\x03\"\x90\x02\n\x06Player\x12\n\n\x02id\x18\x01 \x02(\r\x12\x11\n\tfirstName\x18\x02 \x02(\t\x12\x10\n\x08lastName\x18\x03 \x02(\t\x12\x10\n\x08\x64istance\x18\x04 \x01(\r\x12\x0c\n\x04time\x18\x05 \x01(\r\x12\n\n\x02\x66\x36\x18\x06 \x01(\r\x12\n\n\x02\x66\x37\x18\x07 \x01(\r\x12\n\n\x02\x66\x38\x18\x08 \x01(\r\x12\n\n\x02\x66\x39\x18\t \x01(\r\x12\x0b\n\x03\x66\x31\x30\x18\n \x01(\r\x12\x0b\n\x03\x66\x31\x31\x18\x0b \x01(\r\x12\r\n\x05power\x18\x0c \x01(\r\x12\x0b\n\x03\x66\x31\x33\x18\r \x01(\r\x12\t\n\x01x\x18\x0e \x01(\x02\x12\x10\n\x08\x61ltitude\x18\x0f \x01(\x02\x12\t\n\x01y\x18\x10 \x01(\x02\x12\r\n\x05route\x18\x11 \x01(\x04\x12\x0b\n\x03\x66\x31\x38\x18\x12 \x01(\r\x12\x0b\n\x03\x66\x31\x39\x18\x13 \x01(\r') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0bworld.proto\x1a\rprofile.proto\"\x9d\x02\n\x0b\x44ropInWorld\x12\n\n\x02id\x18\x01 \x02(\x04\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x11\n\tcourse_id\x18\x03 \x01(\x04\x12\n\n\x02\x66\x34\x18\x04 \x01(\x08\x12\x10\n\x08zwifters\x18\x05 \x01(\x04\x12\x12\n\nworld_time\x18\x06 \x01(\x04\x12\x11\n\treal_time\x18\x07 \x01(\x04\x12\"\n\x0bpro_players\x18\x08 \x03(\x0b\x32\r.DropInPlayer\x12 \n\tfollowees\x18\t \x03(\x0b\x32\r.DropInPlayer\x12\x1d\n\x06others\x18\n \x03(\x0b\x32\r.DropInPlayer\x12\x14\n\x0cmax_zwifters\x18\x0b \x01(\x04\x12!\n\npacer_bots\x18\x0c \x03(\x0b\x32\r.DropInPlayer\"/\n\x0f\x44ropInWorldList\x12\x1c\n\x06worlds\x18\x01 \x03(\x0b\x32\x0c.DropInWorld\"\xcc\x02\n\x0c\x44ropInPlayer\x12\n\n\x02id\x18\x01 \x02(\x04\x12\x11\n\tfirstName\x18\x02 \x02(\t\x12\x10\n\x08lastName\x18\x03 \x02(\t\x12\x10\n\x08\x64istance\x18\x04 \x01(\x04\x12\x0c\n\x04time\x18\x05 \x01(\x04\x12\x14\n\x0c\x63ountry_code\x18\x06 \x01(\x04\x12 \n\x0bplayer_type\x18\x07 \x01(\x0e\x32\x0b.PlayerType\x12\x15\n\x05sport\x18\x08 \x01(\x0e\x32\x06.Sport\x12\n\n\x02\x66\x39\x18\t \x01(\x08\x12\x0b\n\x03\x66\x31\x30\x18\n \x01(\x08\x12\x0b\n\x03\x66\x31\x31\x18\x0b \x01(\x04\x12\r\n\x05power\x18\x0c \x01(\x04\x12\x0b\n\x03\x66\x31\x33\x18\r \x01(\x04\x12\t\n\x01x\x18\x0e \x01(\x02\x12\x12\n\ny_altitude\x18\x0f \x01(\x02\x12\t\n\x01z\x18\x10 \x01(\x02\x12\r\n\x05route\x18\x11 \x01(\x05\x12\x12\n\nride_power\x18\x12 \x01(\r\x12\r\n\x05speed\x18\x13 \x01(\r') -_WORLD = DESCRIPTOR.message_types_by_name['World'] -_WORLDS = DESCRIPTOR.message_types_by_name['Worlds'] -_WORLDATTRIBUTES = DESCRIPTOR.message_types_by_name['WorldAttributes'] -_PLAYER = DESCRIPTOR.message_types_by_name['Player'] -World = _reflection.GeneratedProtocolMessageType('World', (_message.Message,), { - 'DESCRIPTOR' : _WORLD, +_DROPINWORLD = DESCRIPTOR.message_types_by_name['DropInWorld'] +_DROPINWORLDLIST = DESCRIPTOR.message_types_by_name['DropInWorldList'] +_DROPINPLAYER = DESCRIPTOR.message_types_by_name['DropInPlayer'] +DropInWorld = _reflection.GeneratedProtocolMessageType('DropInWorld', (_message.Message,), { + 'DESCRIPTOR' : _DROPINWORLD, '__module__' : 'world_pb2' - # @@protoc_insertion_point(class_scope:World) + # @@protoc_insertion_point(class_scope:DropInWorld) }) -_sym_db.RegisterMessage(World) +_sym_db.RegisterMessage(DropInWorld) -Worlds = _reflection.GeneratedProtocolMessageType('Worlds', (_message.Message,), { - 'DESCRIPTOR' : _WORLDS, +DropInWorldList = _reflection.GeneratedProtocolMessageType('DropInWorldList', (_message.Message,), { + 'DESCRIPTOR' : _DROPINWORLDLIST, '__module__' : 'world_pb2' - # @@protoc_insertion_point(class_scope:Worlds) + # @@protoc_insertion_point(class_scope:DropInWorldList) }) -_sym_db.RegisterMessage(Worlds) +_sym_db.RegisterMessage(DropInWorldList) -WorldAttributes = _reflection.GeneratedProtocolMessageType('WorldAttributes', (_message.Message,), { - 'DESCRIPTOR' : _WORLDATTRIBUTES, +DropInPlayer = _reflection.GeneratedProtocolMessageType('DropInPlayer', (_message.Message,), { + 'DESCRIPTOR' : _DROPINPLAYER, '__module__' : 'world_pb2' - # @@protoc_insertion_point(class_scope:WorldAttributes) + # @@protoc_insertion_point(class_scope:DropInPlayer) }) -_sym_db.RegisterMessage(WorldAttributes) - -Player = _reflection.GeneratedProtocolMessageType('Player', (_message.Message,), { - 'DESCRIPTOR' : _PLAYER, - '__module__' : 'world_pb2' - # @@protoc_insertion_point(class_scope:Player) - }) -_sym_db.RegisterMessage(Player) +_sym_db.RegisterMessage(DropInPlayer) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None - _WORLD._serialized_start=16 - _WORLD._serialized_end=182 - _WORLDS._serialized_start=184 - _WORLDS._serialized_end=216 - _WORLDATTRIBUTES._serialized_start=218 - _WORLDATTRIBUTES._serialized_end=255 - _PLAYER._serialized_start=258 - _PLAYER._serialized_end=530 + _DROPINWORLD._serialized_start=31 + _DROPINWORLD._serialized_end=316 + _DROPINWORLDLIST._serialized_start=318 + _DROPINWORLDLIST._serialized_end=365 + _DROPINPLAYER._serialized_start=368 + _DROPINPLAYER._serialized_end=700 # @@protoc_insertion_point(module_scope) diff --git a/protobuf/zfiles.proto b/protobuf/zfiles.proto index eaa11e7..ad3a5bd 100644 --- a/protobuf/zfiles.proto +++ b/protobuf/zfiles.proto @@ -1,7 +1,12 @@ syntax = "proto2"; -message ZFile { +message ZFileProto { required uint64 id = 1; required string folder = 2; required string filename = 3; - required uint64 timestamp = 4; + optional string f4 = 4; //NetworkClientImpl::generateZFileGzip: empty + required uint64 timestamp = 5; +} + +message ZFilesProto { + repeated ZFileProto zfiles = 1; } diff --git a/protobuf/zfiles_pb2.py b/protobuf/zfiles_pb2.py index d1d4421..959a2be 100644 --- a/protobuf/zfiles_pb2.py +++ b/protobuf/zfiles_pb2.py @@ -14,21 +14,31 @@ _sym_db = _symbol_database.Default() -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0czfiles.proto\"H\n\x05ZFile\x12\n\n\x02id\x18\x01 \x02(\x04\x12\x0e\n\x06\x66older\x18\x02 \x02(\t\x12\x10\n\x08\x66ilename\x18\x03 \x02(\t\x12\x11\n\ttimestamp\x18\x04 \x02(\x04') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0czfiles.proto\"Y\n\nZFileProto\x12\n\n\x02id\x18\x01 \x02(\x04\x12\x0e\n\x06\x66older\x18\x02 \x02(\t\x12\x10\n\x08\x66ilename\x18\x03 \x02(\t\x12\n\n\x02\x66\x34\x18\x04 \x01(\t\x12\x11\n\ttimestamp\x18\x05 \x02(\x04\"*\n\x0bZFilesProto\x12\x1b\n\x06zfiles\x18\x01 \x03(\x0b\x32\x0b.ZFileProto') -_ZFILE = DESCRIPTOR.message_types_by_name['ZFile'] -ZFile = _reflection.GeneratedProtocolMessageType('ZFile', (_message.Message,), { - 'DESCRIPTOR' : _ZFILE, +_ZFILEPROTO = DESCRIPTOR.message_types_by_name['ZFileProto'] +_ZFILESPROTO = DESCRIPTOR.message_types_by_name['ZFilesProto'] +ZFileProto = _reflection.GeneratedProtocolMessageType('ZFileProto', (_message.Message,), { + 'DESCRIPTOR' : _ZFILEPROTO, '__module__' : 'zfiles_pb2' - # @@protoc_insertion_point(class_scope:ZFile) + # @@protoc_insertion_point(class_scope:ZFileProto) }) -_sym_db.RegisterMessage(ZFile) +_sym_db.RegisterMessage(ZFileProto) + +ZFilesProto = _reflection.GeneratedProtocolMessageType('ZFilesProto', (_message.Message,), { + 'DESCRIPTOR' : _ZFILESPROTO, + '__module__' : 'zfiles_pb2' + # @@protoc_insertion_point(class_scope:ZFilesProto) + }) +_sym_db.RegisterMessage(ZFilesProto) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None - _ZFILE._serialized_start=16 - _ZFILE._serialized_end=88 + _ZFILEPROTO._serialized_start=16 + _ZFILEPROTO._serialized_end=105 + _ZFILESPROTO._serialized_start=107 + _ZFILESPROTO._serialized_end=149 # @@protoc_insertion_point(module_scope) diff --git a/scripts/bot_editor.py b/scripts/bot_editor.py index da48cf4..33d33cd 100644 --- a/scripts/bot_editor.py +++ b/scripts/bot_editor.py @@ -48,7 +48,7 @@ def file_exists(file): PROFILE_FILE = 'profile.bin' if file_exists(PROFILE_FILE): - p = profile_pb2.Profile() + p = profile_pb2.PlayerProfile() with open(PROFILE_FILE, 'rb') as f: p.ParseFromString(f.read()) p.first_name = input("First name: ") diff --git a/scripts/upload_activity.py b/scripts/upload_activity.py index b848546..08645fa 100644 --- a/scripts/upload_activity.py +++ b/scripts/upload_activity.py @@ -134,7 +134,7 @@ def get_player_id(session, access_token): print('Response HTTP Status Code: {status_code}'.format( status_code=response.status_code)) - profile = profile_pb2.Profile() + profile = profile_pb2.PlayerProfile() profile.ParseFromString(response.content) return profile.id diff --git a/standalone.py b/standalone.py index 89b25c0..b037973 100755 --- a/standalone.py +++ b/standalone.py @@ -94,8 +94,8 @@ def save_ghost(name, player_id): try: if not os.path.isdir(folder): os.makedirs(folder) - except: - return + except Exception as exc: + print('save_ghost: %s' % repr(exc)) f = '%s/%s-%s.bin' % (folder, zwift_offline.get_utc_date_time().strftime("%Y-%m-%d-%H-%M-%S"), name) with open(f, 'wb') as fd: fd.write(ghosts.rec.SerializeToString()) @@ -117,8 +117,8 @@ def organize_ghosts(player_id): try: if not os.path.isdir(dest): os.makedirs(dest) - except: - return + except Exception as exc: + print('organize_ghosts: %s' % repr(exc)) os.rename(file, os.path.join(dest, f)) def load_ghosts(player_id, state, ghosts): @@ -228,9 +228,10 @@ class CDNHandler(SimpleHTTPRequestHandler): url = 'http://{}{}'.format(hostname, self.path) req_header = self.parse_headers() resp = requests.get(url, headers=merge_two_dicts(req_header, set_header()), verify=False) - except: - self.send_error(404, 'error trying to proxy') - return + except Exception as exc: + print('Error trying to proxy: %s' % repr(exc)) + self.send_error(404, 'error trying to proxy') + return self.send_response(resp.status_code) self.send_resp_headers(resp) self.wfile.write(resp.content) @@ -257,18 +258,20 @@ class CDNHandler(SimpleHTTPRequestHandler): class TCPHandler(socketserver.BaseRequestHandler): def handle(self): self.data = self.request.recv(1024) - hello = tcp_node_msgs_pb2.TCPHello() + #print("TCPHandler hello: %s" % self.data.hex()) + hello = udp_node_msgs_pb2.ClientToServer() try: - hello.ParseFromString(self.data[4:-4]) - except: + hello.ParseFromString(self.data[4:-4]) #2 bytes: payload length, 1 byte: =0x1 (TcpClient::sendClientToServer) 1 byte: type; payload; 4 bytes: hash + #type: TcpClient::sayHello(=0x0), TcpClient::sendSubscribeToSegment(=0x1), TcpClient::processSegmentUnsubscription(=0x1) + except Exception as exc: + print('TCPHandler ParseFromString exception: %s' % repr(exc)) return # send packet containing UDP server (127.0.0.1) # (very little investigation done into this packet while creating # protobuf structures hence the excessive "details" usage) - msg = tcp_node_msgs_pb2.TCPServerInfo() + msg = udp_node_msgs_pb2.ServerToClient() msg.player_id = hello.player_id - msg.f3 = 0 - servers = msg.servers.add() + msg.world_time = 0 if self.request.getpeername()[0] == '127.0.0.1': # to avoid needing hairpinning udp_node_ip = "127.0.0.1" elif os.path.exists(SERVER_IP_FILE): @@ -276,41 +279,41 @@ class TCPHandler(socketserver.BaseRequestHandler): udp_node_ip = f.read().rstrip('\r\n') else: udp_node_ip = "127.0.0.1" - details1 = servers.details.add() - details1.f1 = 1 - details1.f2 = 6 + details1 = msg.udp_config.relay_addresses.add() + details1.lb_realm = udp_node_msgs_pb2.ZofflineConstants.RealmID + details1.lb_course = 6 # watopia crowd details1.ip = udp_node_ip details1.port = 3022 - details2 = servers.details.add() - details2.f1 = 0 - details2.f2 = 0 + details2 = msg.udp_config.relay_addresses.add() + details2.lb_realm = 0 #generic load balancing realm + details2.lb_course = 0 #generic load balancing course details2.ip = udp_node_ip details2.port = 3022 - servers.f2 = 10 - servers.f3 = 30 - servers.f4 = 3 - other_servers = msg.other_servers.add() - wdetails1 = other_servers.details_wrapper.add() - wdetails1.f1 = 1 - wdetails1.f2 = 6 - details3 = wdetails1.details.add() + msg.udp_config.uc_f2 = 10 + msg.udp_config.uc_f3 = 30 + msg.udp_config.uc_f4 = 3 + wdetails1 = msg.udp_config_vod_1.relay_addresses_vod.add() + wdetails1.lb_realm = udp_node_msgs_pb2.ZofflineConstants.RealmID + wdetails1.lb_course = 6 # watopia crowd + details3 = wdetails1.relay_addresses.add() details3.CopyFrom(details1) - wdetails2 = other_servers.details_wrapper.add() - wdetails2.f1 = 0 - wdetails2.f2 = 0 - details4 = wdetails2.details.add() + wdetails2 = msg.udp_config_vod_1.relay_addresses_vod.add() + wdetails2.lb_realm = 0 #generic load balancing realm + wdetails2.lb_course = 0 #generic load balancing course + details4 = wdetails2.relay_addresses.add() details4.CopyFrom(details2) - other_servers.port = 3022 + msg.udp_config_vod_1.port = 3022 payload = msg.SerializeToString() # Send size of payload as 2 bytes self.request.sendall(struct.pack('!h', len(payload))) self.request.sendall(payload) player_id = hello.player_id - msg = tcp_node_msgs_pb2.RecurringTCPResponse() + #print("TCPHandler for %d" % player_id) + msg = udp_node_msgs_pb2.ServerToClient() msg.player_id = player_id - msg.f3 = 0 - msg.f11 = 1 + msg.world_time = 0 + msg.stc_f11 = True payload = msg.SerializeToString() last_alive_check = int(zwift_offline.get_utc_time()) @@ -318,8 +321,25 @@ class TCPHandler(socketserver.BaseRequestHandler): #Check every 5 seconds for new updates tcpthreadevent.wait(timeout=5) try: + t = int(zwift_offline.get_utc_time()) + + #if ZC need to be registered + if player_id in zwift_offline.zc_connect_queue: # and player_id in online: + zc_params = udp_node_msgs_pb2.ServerToClient() + zc_params.player_id = player_id + zc_params.world_time = 0 + zc_params.zc_local_ip = zwift_offline.zc_connect_queue[player_id][0] + zc_params.zc_local_port = zwift_offline.zc_connect_queue[player_id][1] #21587 + zc_params.zc_protocol = udp_node_msgs_pb2.IPProtocol.TCP #=2 + zc_params_payload = zc_params.SerializeToString() + last_alive_check = t + self.request.sendall(struct.pack('!h', len(zc_params_payload))) + self.request.sendall(zc_params_payload) + #print("TCPHandler register_zc %d %s" % (player_id, zc_params_payload.hex())) + zwift_offline.zc_connect_queue.pop(player_id) + message = udp_node_msgs_pb2.ServerToClient() - message.f1 = 1 + message.server_realm = udp_node_msgs_pb2.ZofflineConstants.RealmID message.player_id = player_id message.world_time = zwift_offline.world_time() @@ -337,7 +357,7 @@ class TCPHandler(socketserver.BaseRequestHandler): self.request.sendall(message_payload) message = udp_node_msgs_pb2.ServerToClient() - message.f1 = 1 + message.server_realm = udp_node_msgs_pb2.ZofflineConstants.RealmID message.player_id = player_id message.world_time = zwift_offline.world_time() @@ -345,8 +365,6 @@ class TCPHandler(socketserver.BaseRequestHandler): for player_update_proto in added_player_updates: player_update_queue[player_id].remove(player_update_proto) - t = int(zwift_offline.get_utc_time()) - #Check if any updates are added and should be sent to client, otherwise just keep alive every 25 seconds if len(message.updates) > 0: last_alive_check = t @@ -357,7 +375,8 @@ class TCPHandler(socketserver.BaseRequestHandler): last_alive_check = t self.request.sendall(struct.pack('!h', len(payload))) self.request.sendall(payload) - except: + except Exception as exc: + print('TCPHandler loop exception: %s' % repr(exc)) break class GhostsVariables: @@ -458,11 +477,11 @@ def remove_inactive(): def get_empty_message(player_id): message = udp_node_msgs_pb2.ServerToClient() - message.f1 = 1 + message.server_realm = udp_node_msgs_pb2.ZofflineConstants.RealmID message.player_id = player_id message.seqno = 1 - message.f5 = 1 - message.f11 = 1 + message.stc_f5 = 1 + message.stc_f11 = 1 message.msgnum = 1 return message @@ -488,7 +507,8 @@ class UDPHandler(socketserver.BaseRequestHandler): try: #If no sensors connected, first byte must be skipped recv.ParseFromString(data[1:-4]) - except: + except Exception as exc: + print('UDPHandler ParseFromString exception: %s' % repr(exc)) return client_address = self.client_address diff --git a/standalone.spec b/standalone.spec index 8a3be8b..8454c03 100644 --- a/standalone.spec +++ b/standalone.spec @@ -6,9 +6,9 @@ import sys sys.modules['FixTk'] = None a = Analysis(['standalone.py'], - pathex=['/home/alexvh/Code/zoffline'], + pathex=['protobuf'], binaries=[], - datas=[('ssl/*', 'ssl'), ('initialize_db.sql', '.'), ('start_lines.csv', '.'), ('variants.txt', '.')], + datas=[('ssl/*', 'ssl'), ('initialize_db.sql', '.'), ('start_lines.csv', '.'), ('game_info.txt', '.'), ('variants.txt', '.')], hiddenimports=[], hookspath=[], runtime_hooks=[], diff --git a/zwift_offline.py b/zwift_offline.py index 3697c64..79b5fb8 100644 --- a/zwift_offline.py +++ b/zwift_offline.py @@ -15,25 +15,29 @@ import threading import re import smtplib, ssl import requests +import json from copy import copy from functools import wraps from io import BytesIO from shutil import copyfile from logging.handlers import RotatingFileHandler +from urllib.parse import unquote import jwt from flask import Flask, request, jsonify, redirect, render_template, url_for, flash, session, abort, make_response, send_file, send_from_directory from flask_login import UserMixin, AnonymousUserMixin, LoginManager, login_user, current_user, login_required, logout_user from gevent.pywsgi import WSGIServer from google.protobuf.descriptor import FieldDescriptor -from google.protobuf.json_format import Parse +from google.protobuf.json_format import MessageToJson, MessageToDict, Parse from protobuf_to_dict import protobuf_to_dict, TYPE_CALLABLE_MAP from flask_sqlalchemy import sqlalchemy, SQLAlchemy from werkzeug.security import generate_password_hash, check_password_hash from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText +sys.path.append(os.path.join(sys.path[0], 'protobuf')) # otherwise import in .proto does not work import protobuf.udp_node_msgs_pb2 as udp_node_msgs_pb2 +import protobuf.tcp_node_msgs_pb2 as tcp_node_msgs_pb2 import protobuf.activity_pb2 as activity_pb2 import protobuf.goal_pb2 as goal_pb2 import protobuf.login_response_pb2 as login_response_pb2 @@ -129,7 +133,8 @@ try: with open('%s/strava-client.txt' % STORAGE_DIR, 'r') as f: client_id = f.readline().rstrip('\r\n') client_secret = f.readline().rstrip('\r\n') -except: +except Exception as exc: + #logger.warn('strava-client: %s' % repr(exc)) client_id = '28117' client_secret = '41b7b7b76d8cfc5dc12ad5f020adfea17da35468' @@ -153,6 +158,7 @@ global_bots = {} global_ghosts = {} ghosts_enabled = {} player_update_queue = {} +zc_connect_queue = {} player_partial_profiles = {} save_ghost = None restarting = False @@ -183,7 +189,8 @@ class User(UserMixin, db.Model): def verify_token(token): try: data = jwt.decode(token, app.config['SECRET_KEY'], algorithms='HS256') - except: + except Exception as exc: + logger.warn('jwt.decode: %s' % repr(exc)) return None id = data.get('user') if id: @@ -287,6 +294,9 @@ def get_online(): return online_in_region +def toSigned(n, byte_count): + return int.from_bytes(n.to_bytes(byte_count, 'little'), 'little', signed=True) + def get_partial_profile(player_id): if not player_id in player_partial_profiles: #Read from disk @@ -299,18 +309,24 @@ def get_partial_profile(player_id): if os.path.isfile(profile_file): try: with open(profile_file, 'rb') as fd: - profile = profile_pb2.Profile() + profile = profile_pb2.PlayerProfile() profile.ParseFromString(fd.read()) partial_profile = PartialProfile() partial_profile.first_name = profile.first_name partial_profile.last_name = profile.last_name partial_profile.country_code = profile.country_code - for f in profile.f114: - if f.id == 1766985504 or f.id == 3273955058: - partial_profile.route = f.number_value + for f in profile.public_attributes: + #0x69520F20=1766985504 - crc32 of "PACE PARTNER - ROUTE" + #TODO: -1021012238: figure out + if f.id == 1766985504 or f.id == -1021012238: #-1021012238 == 3273955058 + if f.number_value >= 0: + partial_profile.route = toSigned(f.number_value, 4) + else: + partial_profile.route = -toSigned(-f.number_value, 4) break player_partial_profiles[player_id] = partial_profile - except: + except Exception as exc: + logger.warn('get_partial_profile: %s' % repr(exc)) return None else: return None return player_partial_profiles[player_id] @@ -321,6 +337,8 @@ def get_course(state): def is_nearby(player_state1, player_state2, range = 100000): + if player_state1 is None or player_state2 is None: + return False try: if player_state1.watchingRiderId == player_state2.id or player_state2.watchingRiderId == player_state1.id: return True @@ -330,14 +348,15 @@ def is_nearby(player_state1, player_state2, range = 100000): x1 = int(player_state1.x) x2 = int(player_state2.x) if x1 - range <= x2 and x1 + range >= x2: - y1 = int(player_state1.y) - y2 = int(player_state2.y) - if y1 - range <= y2 and y1 + range >= y2: - a1 = int(player_state1.altitude) - a2 = int(player_state2.altitude) + z1 = int(player_state1.z) + z2 = int(player_state2.z) + if z1 - range <= z2 and z1 + range >= z2: + a1 = int(player_state1.y_altitude) + a2 = int(player_state2.y_altitude) if a1 - range <= a2 and a1 + range >= a2: return True - except: + except Exception as exc: + logger.warn('is_nearby: %s' % repr(exc)) pass return False @@ -463,13 +482,31 @@ def forgot(): server.sendmail(sender_email, username, message.as_string()) server.close() flash("E-mail sent.") - except: + except Exception as exc: + logger.warn('send e-mail: %s' % repr(exc)) flash("Could not send e-mail.") else: flash("Invalid username.") return render_template("forgot.html") +@app.route("/api/push/fcm//", methods=["POST", "DELETE"]) +@app.route("/api/push/fcm///enables", methods=["PUT"]) +def api_push_fcm_production(type, token): + return '', 500 + +@app.route("/api/users/password-reset/", methods=["POST"]) +@jwt_to_session_cookie +@login_required +def api_users_password_reset(): + password = request.form.get("password-new") + confirm_password = request.form.get("password-confirm") + if password != confirm_password: + return 'passwords not match', 500 + hashed_pwd = generate_password_hash(password, 'sha256') + current_user.pass_hash = hashed_pwd + db.session.commit() + return '', 200 @app.route("/reset//", methods=["GET", "POST"]) @login_required @@ -498,7 +535,8 @@ def reset(username): def strava(): try: from stravalib.client import Client - except ImportError: + except ImportError as exc: + logger.warn('stravalib: %s' % repr(exc)) flash("stravalib is not installed. Skipping Strava authorization attempt.") return redirect('/user/%s/' % current_user.username) client = Client() @@ -523,7 +561,8 @@ def authorization(): f.write(token_response['refresh_token'] + '\n'); f.write(str(token_response['expires_at']) + '\n'); flash("Strava authorized. Go to \"Upload\" to remove authorization.") - except: + except Exception as exc: + logger.warn('Strava: %s' % repr(exc)) flash("Strava canceled.") flash("Please close this window and return to Zwift Launcher.") return render_template("strava.html", username=current_user.username) @@ -551,7 +590,8 @@ def profile(username): online_sync.logout(session, refresh_token) os.rename('%s/profile.bin' % SCRIPT_DIR, '%s/profile.bin' % profile_dir) flash("Zwift profile installed locally.") - except: + except Exception as exc: + logger.warn('Zwift profile: %s' % repr(exc)) flash("Error downloading profile.") if request.form.get("safe_zwift", None) != None: try: @@ -567,9 +607,11 @@ def profile(username): with open(file_path, 'wb') as fw: fw.write(ciphered_text) flash("Zwift credentials saved.") - except: - flash("Error saving 'zwift_credentiasl.txt' file.") - except: + except Exception as exc: + logger.warn('zwift_credentials: %s' % repr(exc)) + flash("Error saving 'zwift_credentials.txt' file.") + except Exception as exc: + logger.warn('online_sync.login: %s' % repr(exc)) flash("Invalid username or password.") return render_template("profile.html", username=current_user.username) @@ -598,7 +640,8 @@ def garmin(username): with open(file_path, 'wb') as fw: fw.write(ciphered_text) flash("Garmin credentials saved.") - except: + except Exception as exc: + logger.warn('garmin_credentials: %s' % repr(exc)) flash("Error saving 'garmin_credentials.txt' file.") return render_template("garmin.html", username=current_user.username) @@ -609,20 +652,19 @@ def user_home(username): return render_template("user_home.html", username=current_user.username, enable_ghosts=bool(current_user.enable_ghosts), online=get_online(), is_admin=current_user.is_admin, restarting=restarting, restarting_in_minutes=restarting_in_minutes, server_ip=os.path.exists(SERVER_IP_FILE)) - def send_message_to_all_online(message, sender='Server'): - player_update = udp_node_msgs_pb2.PlayerUpdate() - player_update.f2 = 1 - player_update.type = 5 #chat message type - player_update.world_time1 = world_time() - player_update.world_time2 = world_time() + 60000 - player_update.f12 = 1 - player_update.f14 = int(get_utc_time()*1000000) + player_update = udp_node_msgs_pb2.WorldAttribute() + player_update.server_realm = udp_node_msgs_pb2.ZofflineConstants.RealmID + player_update.wa_type = udp_node_msgs_pb2.WA_TYPE.WAT_SPA + player_update.world_time_born = world_time() + player_update.world_time_expire = world_time() + 60000 + player_update.wa_f12 = 1 + player_update.timestamp = int(get_utc_time()*1000000) - chat_message = udp_node_msgs_pb2.ChatMessage() - chat_message.rider_id = 0 - chat_message.to_rider_id = 0 - chat_message.f3 = 1 + chat_message = tcp_node_msgs_pb2.SocialPlayerAction() + chat_message.player_id = 0 + chat_message.to_player_id = 0 + chat_message.spa_type = tcp_node_msgs_pb2.SocialPlayerActionType.SOCIAL_TEXT_MESSAGE chat_message.firstName = sender chat_message.lastName = '' chat_message.message = message @@ -723,7 +765,7 @@ def upload(username): stat = os.stat(profile_file) profile = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(stat.st_mtime)) with open(profile_file, 'rb') as fd: - p = profile_pb2.Profile() + p = profile_pb2.PlayerProfile() p.ParseFromString(fd.read()) name = "%s %s" % (p.first_name, p.last_name) token = None @@ -747,13 +789,21 @@ def upload(username): @app.route("/download/profile.bin", methods=["GET"]) @login_required -def download(): +def download_profile(): player_id = current_user.player_id profile_dir = os.path.join(STORAGE_DIR, str(player_id)) profile_file = os.path.join(profile_dir, 'profile.bin') if os.path.isfile(profile_file): return send_file(profile_file, attachment_filename='profile.bin') +@app.route("/download//avatarLarge.jpg", methods=["GET"]) +def download_avatarLarge(player_id): + profile_dir = os.path.join(STORAGE_DIR, str(player_id)) + profile_file = os.path.join(profile_dir, 'avatarLarge.jpg') + if os.path.isfile(profile_file): + return send_file(profile_file, mimetype='image/jpeg', attachment_filename='avatarLarge.jpg') + else: + return '', 404 @app.route("/delete/", methods=["GET"]) @login_required @@ -801,7 +851,8 @@ def update_protobuf_in_db(table_name, msg, id): id_field = msg.DESCRIPTOR.fields_by_name['id'] if id_field.type == id_field.TYPE_UINT64: id = str(id) - except AttributeError: + except AttributeError as exc: + logger.warn('update_protobuf_in_db: %s' % repr(exc)) pass msg_dict = protobuf_to_dict(msg, type_callable_map=type_callable_map) columns = ', '.join(list(msg_dict.keys())) @@ -841,11 +892,107 @@ def get_id(table_name): def world_time(): return int((get_utc_time()-1414016075)*1000) +@app.route('/api/clubs/club/can-create', methods=['GET']) +def api_clubs_club_cancreate(): + return {"result":False} + +@app.route('/api/event-feed', methods=['GET']) #from=1646723199600&limit=25&sport=CYCLING +def api_eventfeed(): + eventCount = int(request.args.get('limit')) + events = get_events(eventCount) + json_events = convert_events_to_json(events) + json_data = [] + for e in json_events: + json_data.append({"event": e}) + return jsonify({"data":json_data}) + +@app.route('/api/campaign/profile/campaigns', methods=['GET']) +@app.route('/api/notifications', methods=['GET']) +@app.route('/api/announcements/active', methods=['GET']) +def api_empty_arrays(): + return jsonify([]) + +def activity_moving_time(activity): + try: + return (datetime.strptime(activity.end_date, '%y-%m-%dT%H:%M:%SZ') - datetime.strptime(activity.start_date, '%y-%m-%dT%H:%M:%SZ')).total_seconds() * 1000 + except: + return 0 + +def activity_protobuf_to_json(activity): + return {"id":activity.id,"profile":{"id":str(activity.player_id),"firstName":"Youry","lastName":"Pershin","imageSrc":"https://us-or-rly101.zwift.com/download/%s/avatarLarge.jpg" % activity.player_id,"approvalRequired":None}, \ + "worldId":activity.f3,"name":activity.name,"sport":str_sport(activity.f29),"startDate":activity.start_date, \ + "endDate":activity.end_date,"distanceInMeters":activity.distance, \ + "totalElevation":activity.total_elevation,"calories":activity.calories,"primaryImageUrl":"", \ + "feedImageThumbnailUrl":"", \ + "lastSaveDate":activity.date,"movingTimeInMs":activity_moving_time(activity), \ + "avgSpeedInMetersPerSecond":activity.avg_speed,"activityRideOnCount":0,"activityCommentCount":0,"privacy":"PUBLIC", \ + "eventId":None,"rideOnGiven":False,"id_str":str(activity.id)} + +def select_activities_json(player_id, limit): + ret = [] + if limit > 0: + activities = activity_pb2.ActivityList() + # Select every column except 'fit' - despite being a blob python 3 treats it like a utf-8 string and tries to decode it + rows = db.session.execute(sqlalchemy.text("SELECT id, player_id, f3, name, f5, f6, start_date, end_date, distance, avg_heart_rate, max_heart_rate, avg_watts, max_watts, avg_cadence, max_cadence, avg_speed, max_speed, calories, total_elevation, strava_upload_id, strava_activity_id, f23, fit_filename, f29, date FROM activity WHERE player_id = %s ORDER BY date desc LIMIT %s" % (str(player_id), limit))) + allow_empty_end_date = True + for row in rows: + activity = activities.activities.add() + row_to_protobuf(row, activity, exclude_fields=['fit']) + if activity.end_date == "": + if allow_empty_end_date: + allow_empty_end_date = False + else: + continue + ret.append(activity_protobuf_to_json(activity)) + return ret + +@app.route('/api/activity-feed/feed/', methods=['GET']) +@jwt_to_session_cookie +@login_required +def api_activity_feed(): + limit = int(request.args.get('limit')) + feed_type = request.args.get('feedType') + if feed_type == 'JUST_ME' or feed_type == 'PREVIEW': #what is the difference here? + ret = select_activities_json(current_user.player_id, limit) + else: # todo: FAVORITES, FOLLOWEES + ret = [] + return jsonify(ret) @app.route('/api/auth', methods=['GET']) def api_auth(): - return '{"realm":"zwift","launcher":"https://launcher.zwift.com/launcher","url":"https://secure.zwift.com/auth/"}' + return {"realm": "zwift","launcher": "https://launcher.zwift.com/launcher","url": "https://secure.zwift.com/auth/"} +@app.route('/api/server', methods=['GET']) +def api_server(): + return {"build":"zwift_1.267.0","version":"1.267.0"} + +@app.route('/api/servers', methods=['GET']) +def api_servers(): + return {"baseUrl":"https://us-or-rly101.zwift.com/relay"} + +@app.route('/api/clubs/club/list/my-clubs', methods=['GET']) +def api_clubs(): + return {"total":0,"results":[]} + +@app.route('/api/clubs/club/list/my-clubs.proto', methods=['GET']) +@app.route('/api/campaign/proto/campaigns', methods=['GET']) +def api_proto_empty(): + return '', 200 + +@app.route('/api/game_info/version', methods=['GET']) +def api_gameinfo_version(): + game_info_file = os.path.join(SCRIPT_DIR, "game_info.txt") + with open(game_info_file, mode="r", encoding="utf-8-sig") as f: + data = json.load(f) + return {"version": data['gameInfoHash']} + +@app.route('/api/game_info', methods=['GET']) +def api_gameinfo(): + game_info_file = os.path.join(SCRIPT_DIR, "game_info.txt") + with open(game_info_file, mode="r", encoding="utf-8-sig") as f: + r = make_response(f.read()) + r.mimetype = 'application/json' + return r @app.route('/api/users/login', methods=['POST']) def api_users_login(): @@ -856,7 +1003,7 @@ def api_users_login(): response.info.apis.todaysplan_url = "https://whats.todaysplan.com.au" response.info.apis.trainingpeaks_url = "https://api.trainingpeaks.com" response.info.time = int(get_utc_time()) - udp_node = response.info.nodes.node.add() + udp_node = response.info.nodes.nodes.add() if request.remote_addr == '127.0.0.1': # to avoid needing hairpinning udp_node.ip = "127.0.0.1" else: @@ -873,7 +1020,6 @@ def logout_player(player_id): if player_id in player_partial_profiles: player_partial_profiles.pop(player_id) - @app.route('/api/users/logout', methods=['POST']) @jwt_to_session_cookie @login_required @@ -893,32 +1039,63 @@ def api_per_session_info(): info.relay_url = "https://us-or-rly101.zwift.com/relay" return info.SerializeToString(), 200 - -@app.route('/api/events/search', methods=['POST']) -def api_events_search(): - events_list = [('Bologna TT', 2843604888), - ('Crit City CW', 947394567), - ('Crit City CCW', 2875658892), - ('Neokyo Crit', 1127056801), - ('Watopia Waistband', 1064303857)] +def get_events(limit): + events_list = [('Bologna TT', 2843604888, 10), + ('Crit City CW', 947394567, 12), + ('Crit City CCW', 2875658892, 12), + ('Neokyo Crit', 1127056801, 13), + ('Watopia Waistband', 1064303857, 6)] event_id = 1000 + cnt = 0 events = events_pb2.Events() for item in events_list: event = events.events.add() + event.server_realm = udp_node_msgs_pb2.ZofflineConstants.RealmID event.id = event_id - event.title = item[0] - event.route_id = item[1] + event.name = item[0] + event.route_id = item[1] #otherwise new home screen hangs trying to find route in all (even non-existent) courses + event.course_id = item[2] + event.sport = profile_pb2.Sport.CYCLING + event.lateJoinInMinutes = 30 + event.eventStart = int(get_time()) * 1000 + 60000 + cats = ('?', 'A', 'B', 'C', 'D', 'E', 'F') for cat in range(1,5): event_cat = event.category.add() event_cat.id = event_id + cat - event_cat.registrationEnd = int(get_time()) * 1000 + 60000 + event_cat.registrationStart = event.eventStart - 30000 + event_cat.registrationStartWT = world_time() + event_cat.registrationEnd = event.eventStart event_cat.registrationEndWT = world_time() + 60000 + event_cat.lineUpStart = event.eventStart - 15000 + event_cat.lineUpEnd = event.eventStart event_cat.route_id = item[1] event_cat.startLocation = cat event_cat.label = cat + event_cat.lateJoinInMinutes = 30 + event_cat.name = "Cat.%s" % cats[event_cat.label] + event_cat.description = "#zwiftofficial" + event_cat.course_id = event.course_id + event_cat.paceType = 1 + event_cat.fromPaceValue = 1.0 + event_cat.toPaceValue = 5.0 event_id += 1000 - return events.SerializeToString(), 200 + cnt = cnt + 1 + if cnt > limit: + break + return events +@app.route('/api/events/', methods=['GET']) +def api_events_id(event_id): + return '', 200 + +@app.route('/api/events/search', methods=['POST']) +def api_events_search(): + limit = int(request.args.get('limit')) + events = get_events(limit) + if request.headers['Accept'] == 'application/json': + return jsonify(convert_events_to_json(events)) + else: + return events.SerializeToString(), 200 @app.route('/api/events/subgroups/signup/', methods=['POST']) def api_events_subgroups_signup_id(event_id): @@ -943,7 +1120,7 @@ def relay_race_event_starting_line_id(event_id): @app.route('/api/zfiles', methods=['POST']) def api_zfiles(): # Don't care about zfiles, but shuts up some errors in Zwift log. - zfile = zfiles_pb2.ZFile() + zfile = zfiles_pb2.ZFileProto() zfile.id = int(random.getrandbits(31)) zfile.folder = "logfiles" zfile.filename = "yep_took_good_care_of_that_file.txt" @@ -962,6 +1139,17 @@ def custom_style(filename): def static_web_launcher(filename): return send_from_directory('%s/cdn/static/web/launcher' % SCRIPT_DIR, filename) +@app.route('/static/world_headers/') +def static_world_headers(filename): + return send_from_directory('%s/cdn/static/world_headers' % SCRIPT_DIR, filename) + +@app.route('/static/zc/') +def static_zc(filename): + return send_from_directory('%s/cdn/static/zc' % SCRIPT_DIR, filename) + +@app.route('/phoneicons/') +def cdn_phoneicons(filename): + return send_from_directory('%s/cdn/phoneicons' % SCRIPT_DIR, filename) # Probably don't need, haven't investigated @app.route('/api/zfiles/list', methods=['GET', 'POST']) @@ -980,11 +1168,61 @@ def api_private_event_feed(): def api_telemetry_config(): return '{"isEnabled":false}' +def age(dob): + today = datetime.date.today() + years = today.year - dob.year + if today.month < dob.month or (today.month == dob.month and today.day < dob.day): + years -= 1 + return years -@app.route('/api/profiles/me', methods=['GET']) -@jwt_to_session_cookie -@login_required -def api_profiles_me(): +def jsf(obj, field, deflt = None): + if(obj.HasField(field)): + return getattr(obj, field) + return deflt + +def jsb0(obj, field): + return jsf(obj, field, False) + +def jsb1(obj, field): + return jsf(obj, field, True) + +def jsv0(obj, field): + return jsf(obj, field, 0) + +def jses(obj, field): + return str(jsf(obj, field)) + +def copyAttributes(jprofile, jprofileFull, src): + dict = jprofileFull.get(src) + if dict is None: + return + dest = {} + for di in dict: + for v in ['numberValue', 'floatValue', 'stringValue']: + if v in di: + dest[di['id']] = di[v] + jprofile[src] = dest + +def powerSourceModelToStr(val): + if (val == 1): + return "Power Meter" + else: + return "zPower" + +def privacy(profile): + privacy_bits = jsf(profile, 'privacy_bits', 0) + return {"approvalRequired": bool(privacy_bits & 1), "displayWeight": bool(privacy_bits & 4), "minor": bool(privacy_bits & 2), "privateMessaging": bool(privacy_bits & 8), "defaultFitnessDataPrivacy": bool(privacy_bits & 16), +"suppressFollowerNotification": bool(privacy_bits & 32), "displayAge": not bool(privacy_bits & 64), "defaultActivityPrivacy": profile_pb2.ActivityPrivacyType.Name(jsv0(profile, 'default_activity_privacy'))} + +def bikeFrameToStr(val): + if (val == 0x7d8c357d): + return "Zwift Carbon" + else: + if (val == -722210337): + return "Zwift TT" + return "---" + +def do_api_profiles_me(is_json): profile_id = current_user.player_id if MULTIPLAYER: profile_dir = '%s/%s' % (STORAGE_DIR, profile_id) @@ -1008,39 +1246,195 @@ def api_profiles_me(): except IOError as e: logger.error("failed to create profile dir (%s): %s", profile_dir, str(e)) return '', 500 - profile = profile_pb2.Profile() + profile = profile_pb2.PlayerProfile() profile_file = '%s/profile.bin' % profile_dir if not os.path.isfile(profile_file): profile.id = profile_id - profile.is_connected_to_strava = True profile.email = current_user.username profile.first_name = current_user.first_name profile.last_name = current_user.last_name - return profile.SerializeToString(), 200 - with open(profile_file, 'rb') as fd: - profile.ParseFromString(fd.read()) - if MULTIPLAYER: - # For newly added existing profiles, User's player id likely differs from profile's player id. - # If there's existing data in db for this profile, update it for the newly assigned player id. - # XXX: Users can maliciously abuse this by intentionally uploading a profile with another user's current player id. - # However, without it, anyone "upgrading" to multiplayer mode will lose their existing data. - # TODO: need a warning in README that switching to multiplayer mode and back to single player will lose your existing data. - if profile.id != profile_id: - db.session.execute(sqlalchemy.text('UPDATE activity SET player_id = %s WHERE player_id = %s' % (profile_id, profile.id))) - db.session.execute(sqlalchemy.text('UPDATE goal SET player_id = %s WHERE player_id = %s' % (profile_id, profile.id))) - db.session.execute(sqlalchemy.text('UPDATE segment_result SET player_id = %s WHERE player_id = %s' % (profile_id, profile.id))) - db.session.commit() - profile.id = profile_id - elif current_user.player_id != profile.id: - # Update AnonUser's player_id to match - AnonUser.player_id = profile.id - ghosts_enabled[profile.id] = AnonUser.enable_ghosts - if not profile.email: - profile.email = 'user@email.com' - if profile.f60: - del profile.f60[:] + else: + with open(profile_file, 'rb') as fd: + profile.ParseFromString(fd.read()) + if MULTIPLAYER: + # For newly added existing profiles, User's player id likely differs from profile's player id. + # If there's existing data in db for this profile, update it for the newly assigned player id. + # XXX: Users can maliciously abuse this by intentionally uploading a profile with another user's current player id. + # However, without it, anyone "upgrading" to multiplayer mode will lose their existing data. + # TODO: need a warning in README that switching to multiplayer mode and back to single player will lose your existing data. + if profile.id != profile_id: + db.session.execute(sqlalchemy.text('UPDATE activity SET player_id = %s WHERE player_id = %s' % (profile_id, profile.id))) + db.session.execute(sqlalchemy.text('UPDATE goal SET player_id = %s WHERE player_id = %s' % (profile_id, profile.id))) + db.session.execute(sqlalchemy.text('UPDATE segment_result SET player_id = %s WHERE player_id = %s' % (profile_id, profile.id))) + db.session.commit() + profile.id = profile_id + elif current_user.player_id != profile.id: + # Update AnonUser's player_id to match + AnonUser.player_id = profile.id + ghosts_enabled[profile.id] = AnonUser.enable_ghosts + if not profile.email: + profile.email = 'user@email.com' + if profile.entitlements: + del profile.entitlements[:] + if is_json: #todo: publicId, bodyType, totalRunCalories != total_watt_hours, totalRunTimeInMinutes != time_ridden_in_minutes etc + if profile.dob != "": + profile.age = age(datetime.datetime.strptime(profile.dob, "%m/%d/%Y")) + jprofileFull = MessageToDict(profile) + jprofile = {"id": profile.id, "firstName": jsf(profile, 'first_name'), "lastName": jsf(profile, 'last_name'), "preferredLanguage": jsf(profile, 'preferred_language'), "bodyType":jsv0(profile, 'body_type'), "male": jsb1(profile, 'is_male'), +"imageSrc": "https://us-or-rly101.zwift.com/download/%s/avatarLarge.jpg" % profile.id, "imageSrcLarge": "https://us-or-rly101.zwift.com/download/%s/avatarLarge.jpg" % profile.id, "playerType": profile_pb2.PlayerType.Name(jsf(profile, 'player_type', 1)), "playerTypeId": jsf(profile, 'player_type', 1), "playerSubTypeId": None, +"emailAddress": jsf(profile, 'email'), "countryCode": jsf(profile, 'country_code'), "dob": jsf(profile, 'dob'), "countryAlpha3": "rus", "useMetric": jsb1(profile, 'use_metric'), "privacy": privacy(profile), "age": jsv0(profile, 'age'), "ftp": jsf(profile, 'ftp'), "b": False, "weight": jsf(profile, 'weight_in_grams'), "connectedToStrava": jsb0(profile, 'connected_to_strava'), "connectedToTrainingPeaks": jsb0(profile, 'connected_to_training_peaks'), +"connectedToTodaysPlan": jsb0(profile, 'connected_to_todays_plan'), "connectedToUnderArmour": jsb0(profile, 'connected_to_under_armour'), "connectedToFitbit": jsb0(profile, 'connected_to_fitbit'), "connectedToGarmin": jsb0(profile, 'connected_to_garmin'), "height": jsf(profile, 'height_in_millimeters'), "location": "", +"socialFacts": jprofileFull.get('socialFacts'), "totalExperiencePoints": jsv0(profile, 'total_xp'), "worldId": jsf(profile, 'server_realm'), "totalDistance": jsv0(profile, 'total_distance_in_meters'), "totalDistanceClimbed": jsv0(profile, 'elevation_gain_in_meters'), "totalTimeInMinutes": jsv0(profile, 'time_ridden_in_minutes'), +"achievementLevel": jsv0(profile, 'achievement_level'), "totalWattHours": jsv0(profile, 'total_watt_hours'), "runTime1miInSeconds": jsv0(profile, 'run_time_1mi_in_seconds'), "runTime5kmInSeconds": jsv0(profile, 'run_time_5km_in_seconds'), "runTime10kmInSeconds": jsv0(profile, 'run_time_10km_in_seconds'), +"runTimeHalfMarathonInSeconds": jsv0(profile, 'run_time_half_marathon_in_seconds'), "runTimeFullMarathonInSeconds": jsv0(profile, 'run_time_full_marathon_in_seconds'), "totalInKomJersey": jsv0(profile, 'total_in_kom_jersey'), "totalInSprintersJersey": jsv0(profile, 'total_in_sprinters_jersey'), +"totalInOrangeJersey": jsv0(profile, 'total_in_orange_jersey'), "currentActivityId": jsf(profile, 'current_activity_id'), "enrolledZwiftAcademy": jsv0(profile, 'enrolled_program') == profile.EnrolledProgram.ZWIFT_ACADEMY, "runAchievementLevel": jsv0(profile, 'run_achievement_level'), +"totalRunDistance": jsv0(profile, 'total_run_distance'), "totalRunTimeInMinutes": jsv0(profile, 'total_run_time_in_minutes'), "totalRunExperiencePoints": jsv0(profile, 'total_run_experience_points'), "totalRunCalories": jsv0(profile, 'total_run_calories'), "totalGold": jsv0(profile, 'total_gold_drops'), +"profilePropertyChanges": jprofileFull.get('propertyChanges'), "cyclingOrganization": jsf(profile, 'cycling_organization'), "userAgent": "CNL/3.13.0 (Android 11) zwift/1.0.85684 curl/7.78.0-DEV", "stravaPremium": jsb0(profile, 'strava_premium'), "profileChanges": False, "launchedGameClient": "09/19/2021 13:24:19 +0000", +"createdOn":"2021-09-19T13:24:17.783+0000", "likelyInGame": False, "address": None, "bt":"f97803d3-efac-4510-a17a-ef44e65d3071", "numberOfFolloweesInCommon": 0, "fundraiserId": None, "source": "Android", "origin": None, "licenseNumber": None, "bigCommerceId": None, "marketingConsent": None, "affiliate": None, +"avantlinkId": None, "virtualBikeModel": bikeFrameToStr(profile.bike_frame), "connectedToWithings": jsb0(profile, 'connected_to_withings'), "connectedToRuntastic": jsb0(profile, 'connected_to_runtastic'), "connectedToZwiftPower": False, "powerSourceType": "Power Source", "powerSourceModel": powerSourceModelToStr(profile.power_source_model), "riding": False, "location": "", "publicId": "5a72e9b1-239f-435e-8757-af9467336b40", +"mixpanelDistinctId": "21304417-af2d-4c9b-8543-8ba7c0500e84"} + copyAttributes(jprofile, jprofileFull, 'publicAttributes') + copyAttributes(jprofile, jprofileFull, 'privateAttributes') + return jsonify(jprofile) + else: return profile.SerializeToString(), 200 +@app.route('/api/profiles/me', methods=['GET']) +@jwt_to_session_cookie +@login_required +def api_profiles_me_bin(): + if(request.headers['Source'] == "zwift-companion"): + return do_api_profiles_me(True) + else: + return do_api_profiles_me(False) + +@app.route('/api/profiles/me/', methods=['GET']) +@jwt_to_session_cookie +@login_required +def api_profiles_me_json(): + return do_api_profiles_me(True) + +@app.route('/api/partners/garmin/auth', methods=['GET']) +@app.route('/api/partners/trainingpeaks/auth', methods=['GET']) +@app.route('/api/partners/strava/auth', methods=['GET']) +@app.route('/api/partners/withings/auth', methods=['GET']) +@app.route('/api/partners/todaysplan/auth', methods=['GET']) +@app.route('/api/partners/runtastic/auth', methods=['GET']) +@app.route('/api/partners/underarmour/auth', methods=['GET']) +@app.route('/api/partners/fitbit/auth', methods=['GET']) +def api_profiles_partners(): + return {"status":"notConnected","clientId":"zwift","sandbox":False} + +@app.route('/api/profiles//privacy', methods=['POST']) +@jwt_to_session_cookie +@login_required +def api_profiles_id_privacy(player_id): + privacy_file = '%s/%s/privacy.json' % (STORAGE_DIR, player_id) + jp = request.get_json() + with open(privacy_file, 'w', encoding='utf-8') as fprivacy: + fprivacy.write(json.dumps(jp, ensure_ascii=False)) + #{"displayAge": false, "defaultActivityPrivacy": "PUBLIC", "approvalRequired": false, "privateMessaging": false, "defaultFitnessDataPrivacy": false} + profile_dir = '%s/%s' % (STORAGE_DIR, player_id) + profile = profile_pb2.PlayerProfile() + profile_file = '%s/profile.bin' % profile_dir + with open(profile_file, 'rb') as fd: + profile.ParseFromString(fd.read()) + profile.privacy_bits = 0 + if (jp["approvalRequired"]): + profile.privacy_bits += 1 + if ("displayWeight" in jp and jp["displayWeight"]): + profile.privacy_bits += 4 + if ("minor" in jp and jp["minor"]): + profile.privacy_bits += 2 + if (jp["privateMessaging"]): + profile.privacy_bits += 8 + if (jp["defaultFitnessDataPrivacy"]): + profile.privacy_bits += 16 + if ("suppressFollowerNotification" in jp and jp["suppressFollowerNotification"]): + profile.privacy_bits += 32 + if (not jp["displayAge"]): + profile.privacy_bits += 64 + defaultActivityPrivacy = jp["defaultActivityPrivacy"] + profile.default_activity_privacy = 0 #PUBLIC + if(defaultActivityPrivacy == "PRIVATE"): + profile.default_activity_privacy = 1 + if(defaultActivityPrivacy == "FRIENDS"): + profile.default_activity_privacy = 2 + with open(profile_file, 'wb') as fd: + fd.write(profile.SerializeToString()) + return '', 200 + +@app.route('/api/search/profiles', methods=['POST']) +@jwt_to_session_cookie +@login_required +def api_search_profiles(): + query = request.json['query'] + rows = db.session.execute(sqlalchemy.text("SELECT player_id,first_name,last_name FROM user WHERE first_name like '%%%s%%' or last_name like '%%%s%%'" % (query, query))) + json_data_list = []; + for row in rows: + player_id = row[0] + profile_dir = '%s/%s' % (STORAGE_DIR, player_id) + profile = profile_pb2.PlayerProfile() + profile_file = '%s/profile.bin' % profile_dir + with open(profile_file, 'rb') as fd: + profile.ParseFromString(fd.read()) + json_data_list.append({"id": player_id, "firstName": row[1], "lastName": row[2], "imageSrc": "https://us-or-rly101.zwift.com/download/%s/avatarLarge.jpg" % player_id, "imageSrcLarge": "https://us-or-rly101.zwift.com/download/%s/avatarLarge.jpg" % player_id, "countryCode": profile.country_code}) + return jsonify(json_data_list) + +@app.route('/api/profiles//statistics', methods=['GET']) +def api_profiles_id_statistics(player_id): + from_dt = request.args.get('startDateTime') + row = db.session.execute(sqlalchemy.text("SELECT sum(Cast ((JulianDay(date) - JulianDay(start_date)) * 24 * 60 As Integer)), sum(distance), sum(calories), sum(total_elevation) FROM activity WHERE player_id = %s and strftime('%%s', start_date) >= strftime('%%s', '%s')" % (str(player_id), from_dt))).first() + json_data = {"timeRiddenInMinutes": row[0], "distanceRiddenInMeters": row[1], "caloriesBurned": row[2], "heightClimbedInMeters": row[3]} + return jsonify(json_data) + +@app.route('/relay/profiles/me/phone', methods=['PUT']) +@jwt_to_session_cookie +@login_required +def api_profiles_me_phone(): + global zc_connect_queue + if not request.stream: + return '', 400 + phoneAddress = request.json['phoneAddress'] + phonePort = int(request.json['port']) + zc_connect_queue[current_user.player_id] = (phoneAddress, phonePort) + #todo UDP scenario + logger.info("ZCompanion %d reg: %s:%d" % (current_user.player_id, phoneAddress, phonePort)) + return '', 204 + +@app.route('/api/profiles/me/', methods=['PUT']) +@jwt_to_session_cookie +@login_required +def api_profiles_me_id(player_id): + if not request.stream: + return '', 400 + if current_user.player_id != player_id: + return '', 401 + profile_dir = '%s/%s' % (STORAGE_DIR, player_id) + profile = profile_pb2.PlayerProfile() + profile_file = '%s/profile.bin' % profile_dir + with open(profile_file, 'rb') as fd: + profile.ParseFromString(fd.read()) + #update profile from json + profile.country_code = request.json['countryCode'] + profile.dob = request.json['dob'] + profile.email = request.json['emailAddress'] + profile.first_name = request.json['firstName'] + profile.last_name = request.json['lastName'] + profile.height_in_millimeters = request.json['height'] + profile.is_male = request.json['male'] + profile.use_metric = request.json['useMetric'] + profile.weight_in_grams = request.json['weight'] + #profile.large_avatar_url = request.json['imageSrcLarge'] + profile.large_avatar_url = "https://us-or-rly101.zwift.com/download/%s/avatarLarge.jpg" % player_id + #profile.age = request.json['age'] + with open(profile_file, 'wb') as fd: + fd.write(profile.SerializeToString()) + if MULTIPLAYER: + current_user.first_name = profile.first_name + current_user.last_name = profile.last_name + db.session.commit() + return api_profiles_me_json() @app.route('/api/profiles/', methods=['PUT']) @jwt_to_session_cookie @@ -1057,7 +1451,7 @@ def api_profiles_id(player_id): stream = request.stream.read() with open('%s/%s/profile.bin' % (STORAGE_DIR, player_id), 'wb') as f: f.write(stream) - profile = profile_pb2.Profile() + profile = profile_pb2.PlayerProfile() profile.ParseFromString(stream) if MULTIPLAYER: current_user.first_name = profile.first_name @@ -1065,6 +1459,18 @@ def api_profiles_id(player_id): db.session.commit() return '', 204 +@app.route('/api/profiles//photo', methods=['POST']) +@jwt_to_session_cookie +@login_required +def api_profiles_id_photo_post(player_id): + if not request.stream: + return '', 400 + if current_user.player_id != player_id: + return '', 401 + stream = request.stream.read().split(b'\r\n\r\n', maxsplit=1)[1] + with open('%s/%s/avatarLarge.jpg' % (STORAGE_DIR, player_id), 'wb') as f: + f.write(stream) + return '', 200 @app.route('/api/profiles//activities/', methods=['GET', 'POST'], strict_slashes=False) @jwt_to_session_cookie @@ -1082,7 +1488,7 @@ def api_profiles_activities(player_id): return '{"id": %ld}' % activity.id, 200 # request.method == 'GET' - activities = activity_pb2.Activities() + activities = activity_pb2.ActivityList() # Select every column except 'fit' - despite being a blob python 3 treats it like a utf-8 string and tries to decode it rows = db.session.execute(sqlalchemy.text("SELECT id, player_id, f3, name, f5, f6, start_date, end_date, distance, avg_heart_rate, max_heart_rate, avg_watts, max_watts, avg_cadence, max_cadence, avg_speed, max_speed, calories, total_elevation, strava_upload_id, strava_activity_id, f23, fit_filename, f29, date FROM activity WHERE player_id = %s" % str(player_id))) should_remove = list() @@ -1103,10 +1509,10 @@ def api_profiles_activities(player_id): @app.route('/api/profiles', methods=['GET']) def api_profiles(): args = request.args.getlist('id') - profiles = profile_pb2.Profiles() + profiles = profile_pb2.PlayerProfiles() for i in args: p_id = int(i) - profile = profile_pb2.Profile() + profile = profile_pb2.PlayerProfile() if p_id > 10000000: ghostId = math.floor(p_id / 10000000) player_id = p_id - ghostId * 10000000 @@ -1125,20 +1531,20 @@ def api_profiles(): elif seconds < 5259492: span = '%s weeks' % (seconds // 604800) else: span = '%s months' % (seconds // 2629746) p.last_name = span + ' ago [ghost]' - p.f24 = 1456463855 # tron bike + p.bike_frame = 1456463855 # tron bike p.country_code = 0 - if p.f20 == 3761002195: - p.f20 = 1869390707 # basic 2 jersey - p.f27 = 80 # green bike + if p.ride_jersey == 3761002195: + p.ride_jersey = 1869390707 # basic 2 jersey + p.bike_frame_colour = 80 # green bike else: - p.f20 = 3761002195 # basic 4 jersey - p.f27 = 125 # blue bike - if p.f68 == 3344420794: - p.f68 = 4197967370 # shirt 11 - p.f69 = 3273293920 # shorts 11 + p.ride_jersey = 3761002195 # basic 4 jersey + p.bike_frame_colour = 125 # blue bike + if p.run_shirt_type == 3344420794: + p.run_shirt_type = 4197967370 # shirt 11 + p.run_shorts_type = 3273293920 # shorts 11 else: - p.f68 = 3344420794 # shirt 10 - p.f69 = 4269451728 # shorts 10 + p.run_shirt_type = 3344420794 # shirt 10 + p.run_shorts_type = 4269451728 # shorts 10 else: if p_id > 2000000 and p_id < 3000000: profile_file = '%s/%s/profile.bin' % (PACE_PARTNERS_DIR, i) @@ -1158,51 +1564,60 @@ def api_profiles(): def strava_upload(player_id, activity): try: from stravalib.client import Client - except ImportError: + except ImportError as exc: + logger.warn('stravalib: %s' % repr(exc)) logger.warn("stravalib is not installed. Skipping Strava upload attempt.") return profile_dir = '%s/%s' % (STORAGE_DIR, player_id) + strava_token = '%s/strava_token.txt' % profile_dir + if not os.path.exists(strava_token): + logger.info("strava_token.txt missing, skip Strava activity update") + return strava = Client() try: - with open('%s/strava_token.txt' % profile_dir, 'r') as f: + with open(strava_token, 'r') as f: client_id = f.readline().rstrip('\r\n') client_secret = f.readline().rstrip('\r\n') strava.access_token = f.readline().rstrip('\r\n') refresh_token = f.readline().rstrip('\r\n') expires_at = f.readline().rstrip('\r\n') - except: - logger.warn("Failed to read %s/strava_token.txt. Skipping Strava upload attempt." % profile_dir) + except Exception as exc: + logger.warn("Failed to read %s. Skipping Strava upload attempt. %s" % (strava_token, repr(exc))) return try: if get_utc_time() > int(expires_at): refresh_response = strava.refresh_access_token(client_id=client_id, client_secret=client_secret, refresh_token=refresh_token) - with open('%s/strava_token.txt' % profile_dir, 'w') as f: + with open(strava_token, 'w') as f: f.write(client_id + '\n') f.write(client_secret + '\n') f.write(refresh_response['access_token'] + '\n') f.write(refresh_response['refresh_token'] + '\n') f.write(str(refresh_response['expires_at']) + '\n') - except: - logger.warn("Failed to refresh token. Skipping Strava upload attempt.") + except Exception as exc: + logger.warn("Failed to refresh token. Skipping Strava upload attempt: %s." % repr(exc)) return try: # See if there's internet to upload to Strava strava.upload_activity(BytesIO(activity.fit), data_type='fit', name=activity.name) # XXX: assume the upload succeeds on strava's end. not checking on it. - except: - logger.warn("Strava upload failed. No internet?") + except Exception as exc: + logger.warn("Strava upload failed. No internet? %s" % repr(exc)) def garmin_upload(player_id, activity): try: from garmin_uploader.workflow import Workflow - except ImportError: - logger.warn("garmin_uploader is not installed. Skipping Garmin upload attempt.") + except ImportError as exc: + logger.warn("garmin_uploader is not installed. Skipping Garmin upload attempt. %s" % repr(exc)) return profile_dir = '%s/%s' % (STORAGE_DIR, player_id) + garmin_credentials = '%s/garmin_credentials.txt' % profile_dir + if not os.path.exists(garmin_credentials): + logger.info("garmin_credentials.txt missing, skip Garmin activity update") + return try: - with open('%s/garmin_credentials.txt' % profile_dir, 'r') as f: + with open(garmin_credentials, 'r') as f: if credentials_key is not None: cipher_suite = Fernet(credentials_key) ciphered_text = f.read() @@ -1214,52 +1629,60 @@ def garmin_upload(player_id, activity): else: username = f.readline().rstrip('\r\n') password = f.readline().rstrip('\r\n') - except: - logger.warn("Failed to read %s/garmin_credentials.txt. Skipping Garmin upload attempt." % profile_dir) + except Exception as exc: + logger.warn("Failed to read %s. Skipping Garmin upload attempt. %s" % (garmin_credentials, repr(exc))) return try: with open('%s/last_activity.fit' % profile_dir, 'wb') as f: f.write(activity.fit) - except: - logger.warn("Failed to save fit file. Skipping Garmin upload attempt.") + except Exception as exc: + logger.warn("Failed to save fit file. Skipping Garmin upload attempt. %s" % repr(exc)) return try: w = Workflow(['%s/last_activity.fit' % profile_dir], activity_name=activity.name, username=username, password=password) w.run() - except: - logger.warn("Garmin upload failed. No internet?") + except Exception as exc: + logger.warn("Garmin upload failed. No internet? %s" % repr(exc)) def runalyze_upload(player_id, activity): profile_dir = '%s/%s' % (STORAGE_DIR, player_id) + runalyze_token = '%s/runalyze_token.txt' % profile_dir + if not os.path.exists(runalyze_token): + logger.info("runalyze_token.txt missing, skip Runalyze activity update") + return try: - with open('%s/runalyze_token.txt' % profile_dir, 'r') as f: + with open(runalyze_token, 'r') as f: runtoken = f.readline().rstrip('\r\n') - except: - logger.warn("Failed to read %s/runalyze_token.txt. Skipping Runalyze upload attempt." % profile_dir) + except Exception as exc: + logger.warn("Failed to read %s. Skipping Runalyze upload attempt." % (runalyze_token, repr(exc))) return try: with open('%s/last_activity.fit' % profile_dir, 'wb') as f: f.write(activity.fit) - except: - logger.warn("Failed to save fit file. Skipping Runalyze upload attempt.") + except Exception as exc: + logger.warn("Failed to save fit file. Skipping Runalyze upload attempt. %s" % repr(exc)) return try: r = requests.post("https://runalyze.com/api/v1/activities/uploads", files={'file': open('%s/last_activity.fit' % profile_dir, "rb")}, headers={"token": runtoken}) logger.info(r.text) - except: - logger.warn("Runalyze upload failed. No internet?") + except Exception as exc: + logger.warn("Runalyze upload failed. No internet? %s" % repr(exc)) def zwift_upload(player_id, activity): profile_dir = '%s/%s' % (STORAGE_DIR, player_id) SERVER_IP_FILE = "%s/server-ip.txt" % STORAGE_DIR + zwift_credentials = '%s/zwift_credentials.txt' % profile_dir + if not os.path.exists(zwift_credentials): + logger.info("zwift_credentials.txt missing, skip Zwift activity update") + return if not os.path.exists(SERVER_IP_FILE): logger.info("server_ip.txt missing, skip Zwift activity update") return try: - with open('%s/zwift_credentials.txt' % profile_dir, 'r') as f: + with open(zwift_credentials, 'r') as f: if credentials_key is not None: cipher_suite = Fernet(credentials_key) ciphered_text = f.read() @@ -1271,8 +1694,8 @@ def zwift_upload(player_id, activity): else: username = f.readline().rstrip('\r\n') password = f.readline().rstrip('\r\n') - except: - logger.warn("Failed to read %s/zwift_credentials.txt. Skipping Zwift upload attempt." % profile_dir) + except Exception as exc: + logger.warn("Failed to read %s. Skipping Zwift upload attempt. %s" % (zwift_credentials, repr(exc))) return try: @@ -1287,10 +1710,10 @@ def zwift_upload(player_id, activity): else: logger.warn("Zwift activity upload failed:%s:" %res) online_sync.logout(session, refresh_token) - except: - logger.warn("Error uploading activity to Zwift Server") - except: - logger.warn("Zwift upload failed. No internet?") + except Exception as exc: + logger.warn("Error uploading activity to Zwift Server. %s" % repr(exc)) + except Exception as exc: + logger.warn("Zwift upload failed. No internet? %s" % repr(exc)) # With 64 bit ids Zwift can pass negative numbers due to overflow, which the flask int @@ -1304,6 +1727,8 @@ def api_profiles_activities_id(player_id, activity_id): if current_user.player_id != player_id: return '', 401 if request.method == 'DELETE': + db.session.execute(sqlalchemy.text("DELETE FROM activity WHERE id = %s" % activity_id)) + db.session.commit() return 'true', 200 activity_id = int(activity_id) & 0xffffffffffffffff activity = activity_pb2.Activity() @@ -1317,7 +1742,8 @@ def api_profiles_activities_id(player_id, activity_id): if current_user.enable_ghosts: try: save_ghost(activity.name, player_id) - except: + except Exception as exc: + logger.warn('save_ghost: %s' % repr(exc)) pass # For using with upload_activity with open('%s/%s/last_activity.bin' % (STORAGE_DIR, player_id), 'wb') as f: @@ -1338,16 +1764,16 @@ def api_profiles_activities_rideon(recieving_player_id): sending_player_id = request.json['profileId'] profile = get_partial_profile(sending_player_id) if not profile == None: - player_update = udp_node_msgs_pb2.PlayerUpdate() - player_update.f2 = 1 - player_update.type = 4 #ride on type - player_update.world_time1 = world_time() - player_update.world_time2 = player_update.world_time1 + 9890 - player_update.f14 = int(get_utc_time() * 1000000) + player_update = udp_node_msgs_pb2.WorldAttribute() + player_update.server_realm = udp_node_msgs_pb2.ZofflineConstants.RealmID + player_update.wa_type = udp_node_msgs_pb2.WA_TYPE.WAT_RIDE_ON + player_update.world_time_born = world_time() + player_update.world_time_expire = player_update.world_time_born + 9890 + player_update.timestamp = int(get_utc_time() * 1000000) ride_on = udp_node_msgs_pb2.RideOn() - ride_on.rider_id = int(sending_player_id) - ride_on.to_rider_id = int(recieving_player_id) + ride_on.player_id = int(sending_player_id) + ride_on.to_player_id = int(recieving_player_id) ride_on.firstName = profile.first_name ride_on.lastName = profile.last_name ride_on.countryCode = profile.country_code @@ -1364,6 +1790,11 @@ def api_profiles_activities_rideon(recieving_player_id): return '{}', 200 +@app.route('/api/private_event/entitlement', methods=['GET']) +def api_empty_obj(): + return '{}', 200 + +@app.route('/api/profiles//campaigns/otm2020', methods=['GET']) @app.route('/api/profiles//followees', methods=['GET']) def api_profiles_followees(player_id): return '', 200 @@ -1387,17 +1818,18 @@ def unix_time_millis(dt): def fill_in_goal_progress(goal, player_id): - now = get_utc_date_time() + local_now = datetime.datetime.now() + utc_offset = datetime.datetime.fromtimestamp(0) - datetime.datetime.utcfromtimestamp(0) if goal.periodicity == 0: # weekly - first_dt, last_dt = get_week_range(now) + first_dt, last_dt = get_week_range(local_now) else: # monthly - first_dt, last_dt = get_month_range(now) + first_dt, last_dt = get_month_range(local_now) common_sql = ("""FROM activity - WHERE player_id = %s + WHERE player_id = %s AND f29 = %s AND strftime('%s', start_date) >= strftime('%s', '%s') AND strftime('%s', start_date) <= strftime('%s', '%s')""" % - (player_id, '%s', '%s', first_dt, '%s', '%s', last_dt)) + (player_id, goal.f3, '%s', '%s', first_dt - utc_offset, '%s', '%s', last_dt - utc_offset)) if goal.type == 0: # distance distance = db.session.execute(sqlalchemy.text('SELECT SUM(distance) %s' % common_sql)).first()[0] if distance: @@ -1417,12 +1849,88 @@ def fill_in_goal_progress(goal, player_id): goal.actual_distance = 0.0 -def set_goal_end_date(goal, now): +def set_goal_end_date_now(goal): + local_now = datetime.datetime.now() + utc_offset = int((datetime.datetime.fromtimestamp(0) - datetime.datetime.utcfromtimestamp(0)).total_seconds()) if goal.periodicity == 0: # weekly - goal.period_end_date = unix_time_millis(get_week_range(now)[1]) + goal.period_end_date = unix_time_millis(get_week_range(local_now)[1]) - utc_offset else: # monthly - goal.period_end_date = unix_time_millis(get_month_range(now)[1]) + goal.period_end_date = unix_time_millis(get_month_range(local_now)[1]) - utc_offset +def str_sport(int_sport): + if int_sport == 1: + return "RUNNING" + return "CYCLING" + +def sport_from_str(str_sport): + if str_sport == 'CYCLING': + return 0 + return 1 #running + +def str_timestamp(ts): + sec = int(ts/1000) + ms = ts % 1000 + return datetime.datetime.utcfromtimestamp(sec).strftime('%Y-%m-%dT%H:%M:%S.') + str(ms).zfill(3) + "+0000" + +def goalProtobufToJson(goal): + return {"id":goal.id,"profileId":goal.player_id,"sport":str_sport(goal.f3),"name":goal.name,"type":int(goal.type),"periodicity":int(goal.periodicity), +"targetDistanceInMeters":goal.target_distance,"targetDurationInMinutes":goal.target_duration,"actualDistanceInMeters":goal.actual_distance, +"actualDurationInMinutes":goal.actual_duration,"createdOn":str_timestamp(goal.created_on), +"periodEndDate":str_timestamp(goal.period_end_date),"status":int(goal.f13),"timezone":""} + +def goalJsonToProtobuf(json_goal): + goal = goal_pb2.Goal() + goal.f3 = sport_from_str(json_goal['sport']) + goal.id = json_goal['id'] + goal.name = json_goal['name'] + goal.periodicity = int(json_goal['periodicity']) + goal.type = int(json_goal['type']) + goal.f13 = 0 #active + goal.target_distance = json_goal['targetDistanceInMeters'] + goal.target_duration = json_goal['targetDurationInMinutes'] + goal.actual_distance = json_goal['actualDistanceInMeters'] + goal.actual_duration = json_goal['actualDurationInMinutes'] + goal.player_id = json_goal['profileId'] + return goal + +@app.route('/api/profiles//goals/', methods=['PUT']) +@jwt_to_session_cookie +@login_required +def api_profiles_goals_put(player_id, goal_id): + if player_id != current_user.player_id: + return '', 401 + if not request.stream: + return '', 400 + str_goal = request.stream.read() + json_goal = json.loads(str_goal) + goal = goalJsonToProtobuf(json_goal) + update_protobuf_in_db('goal', goal, goal.id) + return jsonify(json_goal) + +def select_protobuf_goals(player_id, limit): + goals = goal_pb2.Goals() + if limit > 0: + rows = db.session.execute(sqlalchemy.text("SELECT * FROM goal WHERE player_id = %s LIMIT %s" % (player_id, limit))) + need_update = list() + for row in rows: + goal = goals.goals.add() + row_to_protobuf(row, goal) + end_dt = datetime.datetime.fromtimestamp(goal.period_end_date / 1000) + now = get_utc_date_time() + if end_dt < now: + need_update.append(goal) + fill_in_goal_progress(goal, player_id) + for goal in need_update: + set_goal_end_date_now(goal) + update_protobuf_in_db('goal', goal, goal.id) + return goals + +def convert_goals_to_json(goals): + json_goals = [] + for goal in goals.goals: + json_goal = goalProtobufToJson(goal) + json_goals.append(json_goal) + return json_goals @app.route('/api/profiles//goals', methods=['GET', 'POST']) @jwt_to_session_cookie @@ -1433,34 +1941,33 @@ def api_profiles_goals(player_id): if request.method == 'POST': if not request.stream: return '', 400 - goal = goal_pb2.Goal() - goal.ParseFromString(request.stream.read()) + if(request.headers['Content-Type'] == 'application/x-protobuf-lite'): + goal = goal_pb2.Goal() + goal.ParseFromString(request.stream.read()) + else: + str_goal = request.stream.read() + json_goal = json.loads(str_goal) + goal = goalJsonToProtobuf(json_goal) goal.id = get_id('goal') now = get_utc_date_time() goal.created_on = unix_time_millis(now) - set_goal_end_date(goal, now) + set_goal_end_date_now(goal) fill_in_goal_progress(goal, player_id) insert_protobuf_into_db('goal', goal) - return goal.SerializeToString(), 200 + if request.headers['Accept'] == 'application/json': + return jsonify(goalProtobufToJson(goal)) + else: + return goal.SerializeToString(), 200 # request.method == 'GET' - goals = goal_pb2.Goals() - rows = db.session.execute(sqlalchemy.text("SELECT * FROM goal WHERE player_id = %s" % player_id)) - need_update = list() - for row in rows: - goal = goals.goals.add() - row_to_protobuf(row, goal) - end_dt = datetime.datetime.fromtimestamp(goal.period_end_date / 1000) - now = get_utc_date_time() - if end_dt < now: - need_update.append(goal) - fill_in_goal_progress(goal, player_id) - for goal in need_update: - set_goal_end_date(goal, now) - update_protobuf_in_db('goal', goal, goal.id) + goals = select_protobuf_goals(player_id, 100) - return goals.SerializeToString(), 200 + if request.headers['Accept'] == 'application/json': + json_goals = convert_goals_to_json(goals) + return jsonify(json_goals) # json for ZCA + else: + return goals.SerializeToString(), 200 # protobuf for ZG @app.route('/api/profiles//goals/', methods=['DELETE']) @@ -1478,13 +1985,13 @@ def api_profiles_goals_id(player_id, goal_id): @app.route('/api/tcp-config', methods=['GET']) @app.route('/relay/tcp-config', methods=['GET']) def api_tcp_config(): - infos = periodic_info_pb2.PeriodicInfos() - info = infos.infos.add() + infos = per_session_info_pb2.TcpConfig() + info = infos.nodes.add() if request.remote_addr == '127.0.0.1': # to avoid needing hairpinning - info.game_server_ip = "127.0.0.1" + info.ip = "127.0.0.1" else: - info.game_server_ip = server_ip - info.f2 = 3023 + info.ip = server_ip + info.port = 3023 return infos.SerializeToString(), 200 @@ -1495,69 +2002,70 @@ def add_player_to_world(player, course_world, is_pace_partner): if not partial_profile == None: online_player = None if is_pace_partner: - online_player = course_world[course_id].pace_partner_states.add() + online_player = course_world[course_id].pacer_bots.add() online_player.route = partial_profile.route - if player.sport == 0: - online_player.f18 = player.power + if player.sport == profile_pb2.Sport.CYCLING: + online_player.ride_power = player.power else: - online_player.f19 = player.speed + online_player.speed = player.speed else: - online_player = course_world[course_id].player_states.add() + online_player = course_world[course_id].others.add() online_player.id = player.id online_player.firstName = partial_profile.first_name online_player.lastName = partial_profile.last_name online_player.distance = player.distance online_player.time = player.time - online_player.f6 = partial_profile.country_code - online_player.f8 = player.sport + online_player.country_code = partial_profile.country_code + online_player.sport = player.sport online_player.power = player.power online_player.x = player.x - online_player.altitude = player.altitude - online_player.y = player.y - course_world[course_id].f5 += 1 + online_player.y_altitude = player.y_altitude + online_player.z = player.z + course_world[course_id].zwifters += 1 -def relay_worlds_generic(world_id=None): +def relay_worlds_generic(server_realm=None): courses = courses_lookup.keys() # Android client also requests a JSON version if request.headers['Accept'] == 'application/json': if request.content_type == 'application/x-protobuf-lite': - #chat_message = udp_node_msgs_pb2.ChatMessage() + #chat_message = tcp_node_msgs_pb2.SocialPlayerAction() #serializedMessage = None try: - player_update = udp_node_msgs_pb2.PlayerUpdate() + player_update = udp_node_msgs_pb2.WorldAttribute() player_update.ParseFromString(request.data) #chat_message.ParseFromString(request.data[6:]) #serializedMessage = chat_message.SerializeToString() - except: + except Exception as exc: + logger.warn('player_update_parse: %s' % repr(exc)) #Not able to decode as playerupdate, send dummy response world = { 'currentDateTime': int(get_utc_time()), 'currentWorldTime': world_time(), 'friendsInWorld': [], - 'mapId': 1, + 'mapId': 1, #maybe, 13 for watopia? 'name': 'Public Watopia', 'playerCount': 0, - 'worldId': 1 + 'worldId': udp_node_msgs_pb2.ZofflineConstants.RealmID } - if world_id: - world['mapId'] = world_id + if server_realm: + world['worldId'] = server_realm return jsonify(world) else: return jsonify([ world ]) #PlayerUpdate - player_update.world_time2 = world_time() + 60000 - player_update.f12 = 1 - player_update.f14 = int(get_utc_time()*1000000) + player_update.world_time_expire = world_time() + 60000 + player_update.wa_f12 = 1 + player_update.timestamp = int(get_utc_time()*1000000) for recieving_player_id in online.keys(): should_receive = False - if player_update.type == 5 or player_update.type == 105: + if player_update.wa_type == udp_node_msgs_pb2.WA_TYPE.WAT_SPA or player_update.wa_type == udp_node_msgs_pb2.WA_TYPE.WAT_SR: recieving_player = online[recieving_player_id] #Chat message - if player_update.type == 5: - chat_message = udp_node_msgs_pb2.ChatMessage() + if player_update.wa_type == udp_node_msgs_pb2.WA_TYPE.WAT_SPA: + chat_message = tcp_node_msgs_pb2.SocialPlayerAction() chat_message.ParseFromString(player_update.payload) - sending_player_id = chat_message.rider_id + sending_player_id = chat_message.player_id if sending_player_id in online: sending_player = online[sending_player_id] #Check that players are on same course and close to each other @@ -1565,9 +2073,9 @@ def relay_worlds_generic(world_id=None): should_receive = True #Segment complete else: - segment_complete = udp_node_msgs_pb2.SegmentComplete() + segment_complete = segment_result_pb2.SegmentResult() segment_complete.ParseFromString(player_update.payload) - sending_player_id = segment_complete.rider_id + sending_player_id = segment_complete.player_id if sending_player_id in online: sending_player = online[sending_player_id] #Check that players are on same course @@ -1580,24 +2088,24 @@ def relay_worlds_generic(world_id=None): if not recieving_player_id in player_update_queue: player_update_queue[recieving_player_id] = list() player_update_queue[recieving_player_id].append(player_update.SerializeToString()) - if player_update.type == 5: - chat_message = udp_node_msgs_pb2.ChatMessage() + if player_update.wa_type == udp_node_msgs_pb2.WA_TYPE.WAT_SPA: + chat_message = tcp_node_msgs_pb2.SocialPlayerAction() chat_message.ParseFromString(player_update.payload) - discord.send_message(chat_message.message, chat_message.rider_id) - return '{}', 200 + discord.send_message(chat_message.message, chat_message.player_id) + return '{}', 200 else: # protobuf request - worlds = world_pb2.Worlds() + worlds = world_pb2.DropInWorldList() world = None course_world = {} for course in courses: world = worlds.worlds.add() - world.id = 1 + world.id = udp_node_msgs_pb2.ZofflineConstants.RealmID world.name = 'Public Watopia' - world.f3 = course + world.course_id = course world.world_time = world_time() world.real_time = int(get_time()) - world.f5 = 0 + world.zwifters = 0 course_world[course] = world for p_id in online.keys(): player = online[p_id] @@ -1610,31 +2118,87 @@ def relay_worlds_generic(world_id=None): bot_variables = global_bots[p_id] bot = bot_variables.route.states[bot_variables.position] add_player_to_world(bot, course_world, False) - if world_id: - world.id = world_id + if server_realm: + world.id = server_realm return world.SerializeToString() else: return worlds.SerializeToString() @app.route('/relay/worlds', methods=['GET']) -@app.route('/relay/dropin', methods=['GET']) +@app.route('/relay/dropin', methods=['GET']) #zwift::protobuf::DropInWorldList def relay_worlds(): return relay_worlds_generic() +def iterableToJson(it): + if it == None: + return None + ret = [] + for i in it: + ret.append(i) + return ret -@app.route('/relay/worlds/', methods=['GET']) -def relay_worlds_id(world_id): - return relay_worlds_generic(world_id) +def eventProtobufToJson(event): + esgs = [] + for event_cat in event.category: + esgs.append({"id":event_cat.id,"name":event_cat.name,"description":event_cat.description,"label":event_cat.label, \ +"subgroupLabel":event_cat.name[-1],"rulesId":event_cat.rules_id,"mapId":event_cat.course_id,"routeId":event_cat.route_id,"routeUrl":event_cat.routeUrl, \ +"jerseyHash":event_cat.jerseyHash,"bikeHash":event_cat.bikeHash,"startLocation":event_cat.startLocation,"invitedLeaders":iterableToJson(event_cat.invitedLeaders), \ +"invitedSweepers":iterableToJson(event_cat.invitedSweepers),"paceType":event_cat.paceType,"fromPaceValue":event_cat.fromPaceValue,"toPaceValue":event_cat.toPaceValue, \ +"fieldLimit":None,"registrationStart":event_cat.registrationStart,"registrationEnd":event_cat.registrationEnd,"lineUpStart":event_cat.lineUpStart, \ +"lineUpEnd":event_cat.lineUpEnd,"eventSubgroupStart":event_cat.eventSubgroupStart,"durationInSeconds":event_cat.durationInSeconds,"laps":event_cat.laps, \ +"distanceInMeters":event_cat.distanceInMeters,"signedUp":False,"signupStatus":1,"registered":False,"registrationStatus":1,"followeeEntrantCount":0, \ +"totalEntrantCount":0,"followeeSignedUpCount":0,"totalSignedUpCount":0,"followeeJoinedCount":0,"totalJoinedCount":0,"auxiliaryUrl":"", \ +"rulesSet":["ALLOWS_LATE_JOIN"],"workoutHash":None,"customUrl":event_cat.customUrl,"overrideMapPreferences":False, \ +"tags":[""],"lateJoinInMinutes":event_cat.lateJoinInMinutes,"timeTrialOptions":None,"qualificationRuleIds":None,"accessValidationResult":None}) + return {"id":event.id,"worldId":event.server_realm,"name":event.name,"description":event.description,"shortName":None,"mapId":event.course_id, \ +"shortDescription":None,"imageUrl":event.imageUrl,"routeId":event.route_id,"rulesId":event.rules_id,"rulesSet":["ALLOWS_LATE_JOIN"], \ +"routeUrl":None,"jerseyHash":event.jerseyHash,"bikeHash":None,"visible":event.visible,"overrideMapPreferences":event.overrideMapPreferences,"eventStart":event.eventStart, "tags":[""], \ +"durationInSeconds":event.durationInSeconds,"distanceInMeters":event.distanceInMeters,"laps":event.laps,"privateEvent":False,"invisibleToNonParticipants":event.invisibleToNonParticipants, \ +"followeeEntrantCount":0,"totalEntrantCount":0,"followeeSignedUpCount":0,"totalSignedUpCount":0,"followeeJoinedCount":0,"totalJoinedCount":0, \ +"eventSeries":None,"auxiliaryUrl":"","imageS3Name":None,"imageS3Bucket":None,"sport":str_sport(event.sport),"cullingType":"CULLING_EVERYBODY", \ +"recurring":True,"recurringOffset":None,"publishRecurring":True,"parentId":None,"type":events_pb2._EVENTTYPEV2.values_by_number[int(event.eventType)].name,"eventType":str(event.eventType), \ +"workoutHash":None,"customUrl":"","restricted":False,"unlisted":False,"eventSecret":None,"accessExpression":None,"qualificationRuleIds":None, \ +"lateJoinInMinutes":event.lateJoinInMinutes,"timeTrialOptions":None,"microserviceName":None,"microserviceExternalResourceId":None, \ +"microserviceEventVisibility":None, "minGameVersion":None,"recordable":True,"imported":False,"eventTemplateId":None, "eventSubgroups": esgs } + +def convert_events_to_json(events): + json_events = [] + for e in events.events: + json_event = eventProtobufToJson(e) + json_events.append(json_event) + return json_events -@app.route('/relay/worlds//join', methods=['POST']) -def relay_worlds_id_join(world_id): +#todo: followingCount=3&pendingEventInviteCount=50&acceptedEventInviteCount=3&playerTotal=true&playerSport=all&eventSport=CYCLING&fetchCampaign=true +@app.route('/relay/worlds//aggregate/mobile', methods=['GET']) +@jwt_to_session_cookie +@login_required +def relay_worlds_id_aggregate_mobile(server_realm): + goalCount = int(request.args.get('goalCount')) + goals = select_protobuf_goals(current_user.player_id, goalCount) + json_goals = convert_goals_to_json(goals) + activityCount = int(request.args.get('activityCount')) + json_activities = select_activities_json(current_user.player_id, activityCount) + eventCount = int(request.args.get('eventCount')) + events = get_events(eventCount) + json_events = convert_events_to_json(events) + return jsonify({"events":json_events,"goals":json_goals,"activities":json_activities,"pendingPrivateEventFeed":[],"acceptedPrivateEventFeed":[],"hasFolloweesToRideOn":False, \ + "worldName":"MAKURIISLANDS","playerCount":0,"followingPlayerCount":0,"followingPlayers":[]}) + +@app.route('/relay/worlds/', methods=['GET']) +@app.route('/relay/worlds//', methods=['GET']) +def relay_worlds_id(server_realm): + return relay_worlds_generic(server_realm) + + +@app.route('/relay/worlds//join', methods=['POST']) +def relay_worlds_id_join(server_realm): return '{"worldTime":%ld}' % world_time() -@app.route('/relay/worlds//players/', methods=['GET']) -def relay_worlds_id_players_id(world_id, player_id): +@app.route('/relay/worlds//players/', methods=['GET']) +def relay_worlds_id_players_id(server_realm, player_id): if player_id in online.keys(): player = online[player_id] return player.SerializeToString() @@ -1647,11 +2211,10 @@ def relay_worlds_id_players_id(world_id, player_id): if player_id in global_bots.keys(): bot = global_bots[player_id] return bot.route.states[bot.position].SerializeToString() - return None + return "" - -@app.route('/relay/worlds//my-hash-seeds', methods=['GET']) -def relay_worlds_my_hash_seeds(world_id): +@app.route('/relay/worlds//my-hash-seeds', methods=['GET']) +def relay_worlds_my_hash_seeds(server_realm): return '[{"expiryDate":196859639979,"seed1":-733221030,"seed2":-2142448243},{"expiryDate":196860425476,"seed1":1528095532,"seed2":-2078218472},{"expiryDate":196862212008,"seed1":1794747796,"seed2":-1901929955},{"expiryDate":196862637148,"seed1":-1411883466,"seed2":1171710140},{"expiryDate":196863874267,"seed1":670195825,"seed2":-317830991}]' @@ -1667,46 +2230,47 @@ def relay_worlds_hash_seeds(): # XXX: attributes have not been thoroughly investigated -@app.route('/relay/worlds//attributes', methods=['POST']) -def relay_worlds_id_attributes(world_id): +@app.route('/relay/worlds//attributes', methods=['POST']) +def relay_worlds_id_attributes(server_realm): # NOTE: This was previously a protobuf message in Zwift client, but later changed. # attribs = world_pb2.WorldAttributes() # attribs.world_time = world_time() # return attribs.SerializeToString(), 200 - return relay_worlds_generic(world_id) + return relay_worlds_generic(server_realm) @app.route('/relay/worlds/attributes', methods=['POST']) def relay_worlds_attributes(): # PlayerUpdate was previously a json request handled in relay_worlds_generic() # now it's protobuf posted to this new route (at least in Windows client) - player_update = udp_node_msgs_pb2.PlayerUpdate() + player_update = udp_node_msgs_pb2.WorldAttribute() try: player_update.ParseFromString(request.data) - except: + except Exception as exc: + logger.warn('player_update_parse: %s' % repr(exc)) return '', 422 - player_update.world_time2 = world_time() + 60000 - player_update.f12 = 1 - player_update.f14 = int(get_utc_time() * 1000000) + player_update.world_time_expire = world_time() + 60000 + player_update.wa_f12 = 1 + player_update.timestamp = int(get_utc_time() * 1000000) for receiving_player_id in online.keys(): should_receive = False - if player_update.type in [5, 105]: + if player_update.wa_type in [udp_node_msgs_pb2.WA_TYPE.WAT_SPA, udp_node_msgs_pb2.WA_TYPE.WAT_SR]: receiving_player = online[receiving_player_id] # Chat message - if player_update.type == 5: - chat_message = udp_node_msgs_pb2.ChatMessage() + if player_update.wa_type == udp_node_msgs_pb2.WA_TYPE.WAT_SPA: + chat_message = tcp_node_msgs_pb2.SocialPlayerAction() chat_message.ParseFromString(player_update.payload) - sending_player_id = chat_message.rider_id + sending_player_id = chat_message.player_id if sending_player_id in online: sending_player = online[sending_player_id] if is_nearby(sending_player, receiving_player): should_receive = True # Segment complete else: - segment_complete = udp_node_msgs_pb2.SegmentComplete() + segment_complete = segment_result_pb2.SegmentResult() segment_complete.ParseFromString(player_update.payload) - sending_player_id = segment_complete.rider_id + sending_player_id = segment_complete.player_id if sending_player_id in online: sending_player = online[sending_player_id] if get_course(sending_player) == get_course(receiving_player) or receiving_player.watchingRiderId == sending_player_id: @@ -1719,10 +2283,10 @@ def relay_worlds_attributes(): player_update_queue[receiving_player_id] = list() player_update_queue[receiving_player_id].append(player_update.SerializeToString()) # If it's a chat message, send to Discord - if player_update.type == 5: - chat_message = udp_node_msgs_pb2.ChatMessage() + if player_update.wa_type == udp_node_msgs_pb2.WA_TYPE.WAT_SPA: + chat_message = tcp_node_msgs_pb2.SocialPlayerAction() chat_message.ParseFromString(player_update.payload) - discord.send_message(chat_message.message, chat_message.rider_id) + discord.send_message(chat_message.message, chat_message.player_id) return '', 201 @@ -1777,7 +2341,7 @@ def handle_segment_results(request): result.finish_time_str = get_utc_date_time().strftime("%Y-%m-%dT%H:%M:%SZ") result.f20 = 0 insert_protobuf_into_db('segment_result', result) - return '{"id": %ld}' % result.id, 200 + return {"id": result.id} # request.method == GET # world_id = int(request.args.get('world_id')) @@ -1792,7 +2356,7 @@ def handle_segment_results(request): to_date = request.args.get('to') results = segment_result_pb2.SegmentResults() - results.world_id = 1 + results.server_realm = udp_node_msgs_pb2.ZofflineConstants.RealmID results.segment_id = segment_id if player_id: @@ -1826,8 +2390,8 @@ def live_segment_results_service_leaders(): return '', 200 -@app.route('/relay/worlds//leave', methods=['POST']) -def relay_worlds_leave(world_id): +@app.route('/relay/worlds//leave', methods=['POST']) +def relay_worlds_leave(server_realm): return '{"worldtime":%ld}' % world_time() @@ -1845,7 +2409,7 @@ def move_old_profile(): profile_file = '%s/profile.bin' % STORAGE_DIR if os.path.isfile(profile_file): with open(profile_file, 'rb') as fd: - profile = profile_pb2.Profile() + profile = profile_pb2.PlayerProfile() profile.ParseFromString(fd.read()) profile_dir = '%s/%s' % (STORAGE_DIR, profile.id) try: @@ -1884,8 +2448,8 @@ def init_database(): except: try: # Fall back to a temporary dir copyfile(DATABASE_PATH, "%s/zwift-offline.db.v%s.%d.bak" % (tempfile.gettempdir(), version, int(get_utc_time()))) - except: - logging.warn("Failed to create a zoffline database backup prior to upgrading it.") + except Exception as exc: + logging.warn("Failed to create a zoffline database backup prior to upgrading it. %s" % repr(exc)) if version < 1: # Adjust old world_time values in segment results to new rough estimate of Zwift's @@ -1990,7 +2554,7 @@ def fake_jwt_with_session_cookie(session_cookie): refresh_token = fake_refresh_token_with_session_cookie(session_cookie) - return """{"access_token":"%s","expires_in":1000021600,"refresh_expires_in":611975560,"refresh_token":"%s","token_type":"bearer","id_token":"%s","not-before-policy":1408478984,"session_state":"0846ab9a-765d-4c3f-a20c-6cac9e86e5f3","scope":""}""" % (access_token, refresh_token, ID_TOKEN) + return {"access_token":access_token,"expires_in":1000021600,"refresh_expires_in":611975560,"refresh_token":refresh_token,"token_type":"bearer","id_token":ID_TOKEN,"not-before-policy":1408478984,"session_state":"0846ab9a-765d-4c3f-a20c-6cac9e86e5f3","scope":""} @app.route('/auth/realms/zwift/protocol/openid-connect/token', methods=['POST']) @@ -2013,19 +2577,26 @@ def auth_realms_zwift_protocol_openid_connect_token(): # Original code argument is replaced with session cookie from launcher refresh_token = jwt.decode(request.form['code'][19:], options=({'verify_signature': False, 'verify_aud': False})) session_cookie = refresh_token['session_cookie'] - return fake_jwt_with_session_cookie(session_cookie), 200 + return jsonify(fake_jwt_with_session_cookie(session_cookie)), 200 elif "refresh_token" in request.form: token = jwt.decode(request.form['refresh_token'], options=({'verify_signature': False, 'verify_aud': False})) - return fake_jwt_with_session_cookie(token['session_cookie']) + return jsonify(fake_jwt_with_session_cookie(token['session_cookie'])) else: # android login current_user.enable_ghosts = user.enable_ghosts ghosts_enabled[current_user.player_id] = current_user.enable_ghosts from flask_login import encode_cookie # cookie is not set in request since we just logged in so create it. - return fake_jwt_with_session_cookie(encode_cookie(str(session['_user_id']))), 200 + return jsonify(fake_jwt_with_session_cookie(encode_cookie(str(session['_user_id'])))), 200 else: AnonUser.enable_ghosts = os.path.exists(ENABLEGHOSTS_FILE) - return FAKE_JWT, 200 + r = make_response(FAKE_JWT) + r.mimetype = 'application/json' + return r + +@app.route('/auth/realms/zwift/protocol/openid-connect/logout', methods=['POST']) +def auth_realms_zwift_protocol_openid_connect_logout(): + # This is called on ZCA logout, we don't want the game client to logout (anyway jwt.decode would fail) + return '', 204 @app.route("/start-zwift" , methods=['POST']) @login_required @@ -2076,6 +2647,14 @@ def experimentation_v1_variant(): Parse(f.read(), variants) return variants.SerializeToString(), 200 +def get_profile_saved_game_achiev2_40_bytes(): + profile_file = '%s/%s/profile.bin' % (STORAGE_DIR, current_user.player_id) + if not os.path.isfile(profile_file): + return b'' + with open(profile_file, 'rb') as fd: + profile = profile_pb2.PlayerProfile() + profile.ParseFromString(fd.read()) + return profile.saved_game[0x110:0x110+0x40] #0x110 = accessories1_100 + 2x8-byte headers @app.route('/achievement/loadPlayerAchievements', methods=['GET']) @jwt_to_session_cookie @@ -2083,7 +2662,13 @@ def experimentation_v1_variant(): def achievement_loadPlayerAchievements(): achievements_file = os.path.join(STORAGE_DIR, str(current_user.player_id), 'achievements.bin') if not os.path.isfile(achievements_file): - return '', 200 + converted = profile_pb2.Achievements() + old_achiev_bits = get_profile_saved_game_achiev2_40_bytes() + for ach_id in range(8 * len(old_achiev_bits)): + if (old_achiev_bits[ach_id // 8] >> (ach_id % 8)) & 0x1: + converted.achievements.add().id = ach_id + with open(achievements_file, 'wb') as f: + f.write(converted.SerializeToString()) with open(achievements_file, 'rb') as f: return f.read(), 200