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