diff --git a/game_info.txt b/game_info.txt new file mode 100644 index 0000000..c8f02d5 --- /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":false,"supportedLaps":true,"leadinAscentInMeters":3.1,"leadinDistanceInMeters":689.5,"blockedForMeetups":0,"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":false,"supportedLaps":true,"leadinAscentInMeters":6.2,"leadinDistanceInMeters":2382.6,"blockedForMeetups":0,"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":false,"supportedLaps":true,"leadinAscentInMeters":9.4,"leadinDistanceInMeters":5658.0,"blockedForMeetups":0,"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":"Surfin' Safari","id":4232726854,"distanceInMeters":9770.0,"distanceInMetersFromEventStart":0.0,"ascentInMeters":43.4,"locKey":"LOC_ROUTE_WATOPIA_SURFINSAFARI","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Surfin_Safari.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":false,"leadinAscentInMeters":0.0,"leadinDistanceInMeters":197.1,"blockedForMeetups":1,"sports":["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":false,"supportedLaps":true,"leadinAscentInMeters":6.2,"leadinDistanceInMeters":2382.6,"blockedForMeetups":0,"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":false,"supportedLaps":true,"leadinAscentInMeters":3.0,"leadinDistanceInMeters":788.9,"blockedForMeetups":0,"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":"Splash and Dash","id":1988847319,"distanceInMeters":6029.6,"distanceInMetersFromEventStart":0.0,"ascentInMeters":26.3,"locKey":"LOC_ROUTE_WATOPIA_SPLASHDASH","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/WATOPIA_Splash_and_Dash.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":false,"leadinAscentInMeters":0.3,"leadinDistanceInMeters":217.1,"blockedForMeetups":1,"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"]}]},{"name":"GRAVEL MOUNTAIN","routes":[{"name":"Gravel Mountain Reverse","id":2956533021,"distanceInMeters":5180.9,"distanceInMetersFromEventStart":0.0,"ascentInMeters":52.6,"locKey":"LOC_ROUTE_GRAVEL_MOUNTAIN_GRAVEL_MOUNTAIN_REVERSE_3","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/GRAVEL_MOUNTAIN_Gravel_Mountain_Reverse.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":false,"leadinAscentInMeters":0.0,"leadinDistanceInMeters":0.0,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]},{"name":"Gravel Mountain","id":3687150686,"distanceInMeters":5180.9,"distanceInMetersFromEventStart":0.0,"ascentInMeters":52.6,"locKey":"LOC_ROUTE_GRAVEL_MOUNTAIN_GRAVEL_MOUNTAIN","imageUrl":"https://cdn.zwift.com/static/zc/ROUTES/GRAVEL_MOUNTAIN_Gravel_Mountain.png","levelLocked":0,"publicEventsOnly":true,"supportedLaps":false,"leadinAscentInMeters":0.0,"leadinDistanceInMeters":0.0,"blockedForMeetups":0,"sports":["CYCLING","RUNNING"]}]}],"schedules":[{"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"},{"map":"YORKSHIRE","start":"2022-04-01T04:01:00Z"},{"map":"FRANCE","start":"2022-04-02T04:01:00Z"},{"map":"RICHMOND","start":"2022-04-05T04:01:00Z"},{"map":"INNSBRUCK","start":"2022-04-08T04:01:00Z"},{"map":"MAKURIISLANDS","start":"2022-04-09T04:01:00Z"},{"map":"LONDON","start":"2022-04-12T04:01:00Z"},{"map":"YORKSHIRE","start":"2022-04-15T04:01:00Z"},{"map":"RICHMOND","start":"2022-04-16T04:01:00Z"},{"map":"FRANCE","start":"2022-04-18T04:01:00Z"},{"map":"MAKURIISLANDS","start":"2022-04-19T04:01:00Z"},{"map":"INNSBRUCK","start":"2022-04-22T04:01:00Z"},{"map":"YORKSHIRE","start":"2022-04-23T04:01:00Z"},{"map":"LONDON","start":"2022-04-24T04:01:00Z"},{"map":"RICHMOND","start":"2022-04-26T04:01:00Z"},{"map":"FRANCE","start":"2022-04-29T04:01:00Z"},{"map":"RICHMOND","start":"2022-05-01T04:01:00Z"},{"map":"FRANCE","start":"2022-05-04T04:01:00Z"},{"map":"YORKSHIRE","start":"2022-05-06T04:01:00Z"},{"map":"LONDON","start":"2022-05-08T04:01:00Z"},{"map":"RICHMOND","start":"2022-05-11T04:01:00Z"},{"map":"MAKURIISLANDS","start":"2022-05-12T04:01:00Z"},{"map":"INNSBRUCK","start":"2022-05-14T04:01:00Z"},{"map":"MAKURIISLANDS","start":"2022-05-15T04:01:00Z"},{"map":"LONDON","start":"2022-05-18T04:01:00Z"},{"map":"YORKSHIRE","start":"2022-05-19T04:01:00Z"},{"map":"FRANCE","start":"2022-05-20T04:01:00Z"},{"map":"MAKURIISLANDS","start":"2022-05-22T04:01:00Z"},{"map":"INNSBRUCK","start":"2022-05-25T04:01:00Z"},{"map":"LONDON","start":"2022-05-26T04:01:00Z"},{"map":"RICHMOND","start":"2022-05-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"},{"id":185,"name":"SURFIN' SAFARI","imageUrl":"https://cdn.zwift.com/static/zc/ACHIEVEMENTS/RouteComplete.png"},{"id":186,"name":"SPLASH AND DASH","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":119884741,"name":"Velos","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Velos2022_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":310916269,"name":"WattFabrik","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/WattFabrik2022_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":429028749,"name":"Scannellare","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Scannellare2022_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":561363741,"name":"CredoTri","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/CredoTri2022_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":804393467,"name":"Her Spirit","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/HerSpirit2022_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":851470392,"name":"Atom Racing Team","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/AtomRacing2022_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":875545637,"name":"Black Girls Do Bike","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BGDB2022_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":1079776699,"name":"TRINITY Racing Training","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TRINITYTraining2022_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":1147316612,"name":"Plantur-Pura Cycling Team","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/PlanturPura2022_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":"SAINTS","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":1295920261,"name":"Race Leader - Rosa","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_RaceLeaderRosa_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":1494272741,"name":"Team SD Worx","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/SDWorx2022_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":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":1665932906,"name":"Zwift Riders Scotland","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZRScotland2022_thumb.png"},{"id":1677202555,"name":"Indoor Specialist","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/IndoorSpecialist_thumb.png"},{"id":1697837945,"name":"Kiwi Crew","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/KiwiCrew2022_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":1953455256,"name":"Ruhrrider 2022","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/RuhrRiders2022_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":2001275092,"name":"Velocio","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZRLPremierDiv2022_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":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":2193554526,"name":"Valhalla","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Valhalla2022_thumb.png"},{"id":2202078812,"name":"La Bicicletta","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/LaBicicletta_thumb.png"},{"id":2214060235,"name":"Race Leader - Red","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Zwift_RaceLeaderRed_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":2303114199,"name":"Enjoy Your Bike","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/EnjoyYourBike2022_thumb.png"},{"id":2308547097,"name":"Pride 2022","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/PrideOn2022_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":2361564125,"name":"Team Tscherning Cycling Academy","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TCCA2022_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":2389611207,"name":"Varlo Cycling","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Varlo2022_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":2636119382,"name":"Team Greece","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TeamGreece2022_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":2662676193,"name":"Alpentroedler Velo Club","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Alpentroedler2022_thumb.png"},{"id":2668995113,"name":"Vattern Rundan","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/VatternRundan2019_thumb.png"},{"id":2676350104,"name":"The Breakaway","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TheBreakawayApp2022_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":2727136434,"name":"Team Novo Nordisk 2022","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TeamNovoNordisk2022_thumb.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":2756919352,"name":"Ride Like King 14","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/RideLikeAKing2022_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":2761385929,"name":"TriMag.DE","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TriMagde2022_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":2781649419,"name":"Halo ID","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/HaloID2022_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":2848198363,"name":"WMZ","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/WMZ2022_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":2988130623,"name":"LUX Cycling","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/LUXCycling2022_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":3170941906,"name":"B&B Hotels p/b KTM","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/BBHotelsKTM2022_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":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":3266758270,"name":"That Triathlon Life","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TLL2022_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":3301728125,"name":"Spaero Triathlon","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Spaero2022_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":3326266516,"name":"Tour de Tietema 22","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TdTietema2022_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":3365175761,"name":"Yoeleo 4Mind","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Yeoleo2022_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":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":3384559345,"name":"SCOTT-SRAM MTB Racing Team World Champ","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ScottSRAMMTBWC2022_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":3398931495,"name":"Team Roland Cogeas Edelweiss","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TeamRolandCogeas2022_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":3415411505,"name":"Sansego","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/SansegoTri2022_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":3430221138,"name":"2022 ZA Tri Team Design","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ZATri2022_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":3651831027,"name":"TRINITY Racing Race","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/TRINITYRace2022_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":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":3840427844,"name":"Felt Cycling","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/Felt2021_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":3888251545,"name":"SCOTT-SRAM MTB Racing Team","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/ScottSRAMMTB2022_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":4283549492,"name":"KC Academy","imageUrl":"https://cdn.zwift.com/static/zc/JERSEYS/KCAcademy2022_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":"Felt Felt TT 2021","id":11394848},{"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":"Allied AlliedAble2022","id":431820725},{"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":"Scott ScottPlasma2022","id":2002850191},{"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":"Liv LivDevote2022","id":2162282312},{"name":"Giant TCR Advanced SL","id":2205705045},{"name":"Specialized SpecializedAethos2021","id":2346116422},{"name":"Giant GiantRevolt2022","id":2360271970},{"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":"Canyon CanyonSRAMUltimate2022","id":2884593499},{"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":"Scott ScottSparkMTB2022","id":3133382714},{"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":"A2CF0A4641B2EBB28CE355910598F417"} \ 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/Makefile b/protobuf/Makefile index 669c7b7..0390bc3 100644 --- a/protobuf/Makefile +++ b/protobuf/Makefile @@ -4,7 +4,6 @@ all: 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 diff --git a/protobuf/activity.proto b/protobuf/activity.proto index e194db9..a5c5ea2 100644 --- a/protobuf/activity.proto +++ b/protobuf/activity.proto @@ -1,34 +1,200 @@ 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 { //.2,.3,.4 meanings inside last_activity.bin - not type, priority, incidentTime (i.e. 1595, 8, 9) + optional uint64 activity_id = 1; + optional NotableMomentTypeZG type = 2; + optional uint32 priority = 3; + optional uint64 incidentTime = 4; + optional string aux1 = 5; // examples: {"achievementId":35,"name":"PAIRED","description":"Paired a phone through Zwift Companion"} + // {"segmentName":"Marina Sprint","jerseyHash":"0x997d9df7","durationInMS":30524} (from last_activity.bin) + 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..c6077c8 100644 --- a/protobuf/events.proto +++ b/protobuf/events.proto @@ -1,13 +1,74 @@ syntax = "proto2"; +import "profile.proto"; //enums PlayerType and Sport -message Category { +message InvitedProfileProto { + required uint64 player_id = 1; + optional string firstName = 2; + optional string lastName = 3; + optional string imageSrc = 4; + optional bool enrolledZwiftAcademy = 5; + optional bool male = 6; + optional PlayerType player_type = 7; +} + +enum EventInviteStatus { + PENDING = 0; + ACCEPTED = 1; + REJECTED = 2; +} +message EventInviteProto { + optional InvitedProfileProto profile = 1; + required EventInviteStatus status = 2; +} + +message PrivateEventFeedProto { + required uint64 event_id = 1; //read in PrivateEventsManagerV2::RegisterPrivateEvent + optional Sport sport = 2; + optional uint64 eventSubgroupStart = 3; //IsPrivateEventExpired: return ZNETWORK_GetNetworkSyncedTimeGMT() > a1->m_eventSubgroupStart / 1000uLL + 1800 + optional uint64 route_id = 4; + optional uint32 durationInSeconds = 5; + optional float distanceInMeters = 6; + optional uint64 answeredCount = 7; //stored as int32 + optional uint32 invitedTotalCount = 8; + optional uint32 acceptedFolloweeCount = 9; + optional uint32 acceptedTotalCount = 10; + optional string organizerImageUrl = 11; + optional uint64 organizerProfileId = 12; + optional string organizerFirstName = 13; + optional string organizerLastName = 14; + optional uint64 updateDate = 15; + optional uint64 subgroupId = 16; + optional uint32 laps = 17; + optional bool rubberbanding = 18; //keep together "rulesId": "1024" +} + +message PrivateEventFeedListProto { + repeated PrivateEventFeedProto pef = 1; +} + +message PlayerJoinedEvent { + required uint64 rel_id = 1; //subgroup id + optional uint64 player_id = 2; + optional uint64 pje_f3 = 3; +} + +message PlayerLeftEvent { + required uint64 rel_id = 1; //event id + optional uint64 player_id = 2; + optional uint64 ple_f3 = 3; +} + +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 +79,151 @@ 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 almost everywhere, 2 sometimes; 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; // 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? saw = 2 + 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; } + +message PrivateEventProto { + required uint64 id = 1; + optional Sport sport = 2; + optional uint64 eventStart = 3; + optional uint64 routeId = 4; + optional uint64 startLocation = 5; + optional uint32 durationInSeconds = 6; + optional float distanceInMeters = 7; + optional string description = 8; + optional uint64 workoutHash = 9; //or laps + optional uint64 organizerId = 10; + repeated EventInviteProto eventInvites = 11; + optional bool showResults = 12; + optional uint32 laps = 13; //or workoutHash + optional bool rubberbanding = 14; +} \ No newline at end of file diff --git a/protobuf/events_pb2.py b/protobuf/events_pb2.py index b7aff5a..b292d93 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,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\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\"\xad\x01\n\x13InvitedProfileProto\x12\x11\n\tplayer_id\x18\x01 \x02(\x04\x12\x11\n\tfirstName\x18\x02 \x01(\t\x12\x10\n\x08lastName\x18\x03 \x01(\t\x12\x10\n\x08imageSrc\x18\x04 \x01(\t\x12\x1c\n\x14\x65nrolledZwiftAcademy\x18\x05 \x01(\x08\x12\x0c\n\x04male\x18\x06 \x01(\x08\x12 \n\x0bplayer_type\x18\x07 \x01(\x0e\x32\x0b.PlayerType\"]\n\x10\x45ventInviteProto\x12%\n\x07profile\x18\x01 \x01(\x0b\x32\x14.InvitedProfileProto\x12\"\n\x06status\x18\x02 \x02(\x0e\x32\x12.EventInviteStatus\"\xcb\x03\n\x15PrivateEventFeedProto\x12\x10\n\x08\x65vent_id\x18\x01 \x02(\x04\x12\x15\n\x05sport\x18\x02 \x01(\x0e\x32\x06.Sport\x12\x1a\n\x12\x65ventSubgroupStart\x18\x03 \x01(\x04\x12\x10\n\x08route_id\x18\x04 \x01(\x04\x12\x19\n\x11\x64urationInSeconds\x18\x05 \x01(\r\x12\x18\n\x10\x64istanceInMeters\x18\x06 \x01(\x02\x12\x15\n\ransweredCount\x18\x07 \x01(\x04\x12\x19\n\x11invitedTotalCount\x18\x08 \x01(\r\x12\x1d\n\x15\x61\x63\x63\x65ptedFolloweeCount\x18\t \x01(\r\x12\x1a\n\x12\x61\x63\x63\x65ptedTotalCount\x18\n \x01(\r\x12\x19\n\x11organizerImageUrl\x18\x0b \x01(\t\x12\x1a\n\x12organizerProfileId\x18\x0c \x01(\x04\x12\x1a\n\x12organizerFirstName\x18\r \x01(\t\x12\x19\n\x11organizerLastName\x18\x0e \x01(\t\x12\x12\n\nupdateDate\x18\x0f \x01(\x04\x12\x12\n\nsubgroupId\x18\x10 \x01(\x04\x12\x0c\n\x04laps\x18\x11 \x01(\r\x12\x15\n\rrubberbanding\x18\x12 \x01(\x08\"@\n\x19PrivateEventFeedListProto\x12#\n\x03pef\x18\x01 \x03(\x0b\x32\x16.PrivateEventFeedProto\"F\n\x11PlayerJoinedEvent\x12\x0e\n\x06rel_id\x18\x01 \x02(\x04\x12\x11\n\tplayer_id\x18\x02 \x01(\x04\x12\x0e\n\x06pje_f3\x18\x03 \x01(\x04\"D\n\x0fPlayerLeftEvent\x12\x0e\n\x06rel_id\x18\x01 \x02(\x04\x12\x11\n\tplayer_id\x18\x02 \x01(\x04\x12\x0e\n\x06ple_f3\x18\x03 \x01(\x04\"\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\"\xc9\x02\n\x11PrivateEventProto\x12\n\n\x02id\x18\x01 \x02(\x04\x12\x15\n\x05sport\x18\x02 \x01(\x0e\x32\x06.Sport\x12\x12\n\neventStart\x18\x03 \x01(\x04\x12\x0f\n\x07routeId\x18\x04 \x01(\x04\x12\x15\n\rstartLocation\x18\x05 \x01(\x04\x12\x19\n\x11\x64urationInSeconds\x18\x06 \x01(\r\x12\x18\n\x10\x64istanceInMeters\x18\x07 \x01(\x02\x12\x13\n\x0b\x64\x65scription\x18\x08 \x01(\t\x12\x13\n\x0bworkoutHash\x18\t \x01(\x04\x12\x13\n\x0borganizerId\x18\n \x01(\x04\x12\'\n\x0c\x65ventInvites\x18\x0b \x03(\x0b\x32\x11.EventInviteProto\x12\x13\n\x0bshowResults\x18\x0c \x01(\x08\x12\x0c\n\x04laps\x18\r \x01(\r\x12\x15\n\rrubberbanding\x18\x0e \x01(\x08*<\n\x11\x45ventInviteStatus\x12\x0b\n\x07PENDING\x10\x00\x12\x0c\n\x08\x41\x43\x43\x45PTED\x10\x01\x12\x0c\n\x08REJECTED\x10\x02*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') + +_EVENTINVITESTATUS = DESCRIPTOR.enum_types_by_name['EventInviteStatus'] +EventInviteStatus = enum_type_wrapper.EnumTypeWrapper(_EVENTINVITESTATUS) +_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) +PENDING = 0 +ACCEPTED = 1 +REJECTED = 2 +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'] +_INVITEDPROFILEPROTO = DESCRIPTOR.message_types_by_name['InvitedProfileProto'] +_EVENTINVITEPROTO = DESCRIPTOR.message_types_by_name['EventInviteProto'] +_PRIVATEEVENTFEEDPROTO = DESCRIPTOR.message_types_by_name['PrivateEventFeedProto'] +_PRIVATEEVENTFEEDLISTPROTO = DESCRIPTOR.message_types_by_name['PrivateEventFeedListProto'] +_PLAYERJOINEDEVENT = DESCRIPTOR.message_types_by_name['PlayerJoinedEvent'] +_PLAYERLEFTEVENT = DESCRIPTOR.message_types_by_name['PlayerLeftEvent'] +_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, +_PRIVATEEVENTPROTO = DESCRIPTOR.message_types_by_name['PrivateEventProto'] +InvitedProfileProto = _reflection.GeneratedProtocolMessageType('InvitedProfileProto', (_message.Message,), { + 'DESCRIPTOR' : _INVITEDPROFILEPROTO, '__module__' : 'events_pb2' - # @@protoc_insertion_point(class_scope:Category) + # @@protoc_insertion_point(class_scope:InvitedProfileProto) }) -_sym_db.RegisterMessage(Category) +_sym_db.RegisterMessage(InvitedProfileProto) + +EventInviteProto = _reflection.GeneratedProtocolMessageType('EventInviteProto', (_message.Message,), { + 'DESCRIPTOR' : _EVENTINVITEPROTO, + '__module__' : 'events_pb2' + # @@protoc_insertion_point(class_scope:EventInviteProto) + }) +_sym_db.RegisterMessage(EventInviteProto) + +PrivateEventFeedProto = _reflection.GeneratedProtocolMessageType('PrivateEventFeedProto', (_message.Message,), { + 'DESCRIPTOR' : _PRIVATEEVENTFEEDPROTO, + '__module__' : 'events_pb2' + # @@protoc_insertion_point(class_scope:PrivateEventFeedProto) + }) +_sym_db.RegisterMessage(PrivateEventFeedProto) + +PrivateEventFeedListProto = _reflection.GeneratedProtocolMessageType('PrivateEventFeedListProto', (_message.Message,), { + 'DESCRIPTOR' : _PRIVATEEVENTFEEDLISTPROTO, + '__module__' : 'events_pb2' + # @@protoc_insertion_point(class_scope:PrivateEventFeedListProto) + }) +_sym_db.RegisterMessage(PrivateEventFeedListProto) + +PlayerJoinedEvent = _reflection.GeneratedProtocolMessageType('PlayerJoinedEvent', (_message.Message,), { + 'DESCRIPTOR' : _PLAYERJOINEDEVENT, + '__module__' : 'events_pb2' + # @@protoc_insertion_point(class_scope:PlayerJoinedEvent) + }) +_sym_db.RegisterMessage(PlayerJoinedEvent) + +PlayerLeftEvent = _reflection.GeneratedProtocolMessageType('PlayerLeftEvent', (_message.Message,), { + 'DESCRIPTOR' : _PLAYERLEFTEVENT, + '__module__' : 'events_pb2' + # @@protoc_insertion_point(class_scope:PlayerLeftEvent) + }) +_sym_db.RegisterMessage(PlayerLeftEvent) + +EventSubgroupProtobuf = _reflection.GeneratedProtocolMessageType('EventSubgroupProtobuf', (_message.Message,), { + 'DESCRIPTOR' : _EVENTSUBGROUPPROTOBUF, + '__module__' : 'events_pb2' + # @@protoc_insertion_point(class_scope:EventSubgroupProtobuf) + }) +_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, @@ -42,13 +150,50 @@ Events = _reflection.GeneratedProtocolMessageType('Events', (_message.Message,), }) _sym_db.RegisterMessage(Events) +PrivateEventProto = _reflection.GeneratedProtocolMessageType('PrivateEventProto', (_message.Message,), { + 'DESCRIPTOR' : _PRIVATEEVENTPROTO, + '__module__' : 'events_pb2' + # @@protoc_insertion_point(class_scope:PrivateEventProto) + }) +_sym_db.RegisterMessage(PrivateEventProto) + 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 + _EVENTINVITESTATUS._serialized_start=3474 + _EVENTINVITESTATUS._serialized_end=3534 + _EVENTVISIBILITY._serialized_start=3536 + _EVENTVISIBILITY._serialized_end=3622 + _EVENTTYPEV2._serialized_start=3625 + _EVENTTYPEV2._serialized_end=3790 + _EVENTTYPE._serialized_start=3792 + _EVENTTYPE._serialized_end=3892 + _EVENTCULLING._serialized_start=3894 + _EVENTCULLING._serialized_end=4005 + _INVITEDPROFILEPROTO._serialized_start=32 + _INVITEDPROFILEPROTO._serialized_end=205 + _EVENTINVITEPROTO._serialized_start=207 + _EVENTINVITEPROTO._serialized_end=300 + _PRIVATEEVENTFEEDPROTO._serialized_start=303 + _PRIVATEEVENTFEEDPROTO._serialized_end=762 + _PRIVATEEVENTFEEDLISTPROTO._serialized_start=764 + _PRIVATEEVENTFEEDLISTPROTO._serialized_end=828 + _PLAYERJOINEDEVENT._serialized_start=830 + _PLAYERJOINEDEVENT._serialized_end=900 + _PLAYERLEFTEVENT._serialized_start=902 + _PLAYERLEFTEVENT._serialized_end=970 + _EVENTSUBGROUPPROTOBUF._serialized_start=973 + _EVENTSUBGROUPPROTOBUF._serialized_end=1952 + _MICROSERVICEEVENTDATA._serialized_start=1954 + _MICROSERVICEEVENTDATA._serialized_end=2057 + _EVENTSERIESPROTOBUF._serialized_start=2059 + _EVENTSERIESPROTOBUF._serialized_end=2127 + _EVENTTIMETRIALOPTIONS._serialized_start=2129 + _EVENTTIMETRIALOPTIONS._serialized_end=2256 + _EVENT._serialized_start=2259 + _EVENT._serialized_end=3106 + _EVENTS._serialized_start=3108 + _EVENTS._serialized_end=3140 + _PRIVATEEVENTPROTO._serialized_start=3143 + _PRIVATEEVENTPROTO._serialized_end=3472 # @@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..cbe0f4f --- /dev/null +++ b/protobuf/make.bat @@ -0,0 +1,16 @@ +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=. 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 deleted file mode 100644 index 9b7b95e..0000000 --- a/protobuf/periodic-info.proto +++ /dev/null @@ -1,14 +0,0 @@ -syntax = "proto2"; -message PeriodicInfo { - required string game_server_ip = 1; - optional uint32 f2 = 2; - optional uint32 f3 = 3; - optional uint32 f4 = 4; - optional uint32 f5 = 5; - optional uint32 f6 = 6; -} - - -message PeriodicInfos { - repeated PeriodicInfo infos = 1; -} diff --git a/protobuf/periodic_info_pb2.py b/protobuf/periodic_info_pb2.py deleted file mode 100644 index bc7ac85..0000000 --- a/protobuf/periodic_info_pb2.py +++ /dev/null @@ -1,44 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: periodic-info.proto -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import message as _message -from google.protobuf import reflection as _reflection -from google.protobuf import symbol_database as _symbol_database -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x13periodic-info.proto\"b\n\x0cPeriodicInfo\x12\x16\n\x0egame_server_ip\x18\x01 \x02(\t\x12\n\n\x02\x66\x32\x18\x02 \x01(\r\x12\n\n\x02\x66\x33\x18\x03 \x01(\r\x12\n\n\x02\x66\x34\x18\x04 \x01(\r\x12\n\n\x02\x66\x35\x18\x05 \x01(\r\x12\n\n\x02\x66\x36\x18\x06 \x01(\r\"-\n\rPeriodicInfos\x12\x1c\n\x05infos\x18\x01 \x03(\x0b\x32\r.PeriodicInfo') - - - -_PERIODICINFO = DESCRIPTOR.message_types_by_name['PeriodicInfo'] -_PERIODICINFOS = DESCRIPTOR.message_types_by_name['PeriodicInfos'] -PeriodicInfo = _reflection.GeneratedProtocolMessageType('PeriodicInfo', (_message.Message,), { - 'DESCRIPTOR' : _PERIODICINFO, - '__module__' : 'periodic_info_pb2' - # @@protoc_insertion_point(class_scope:PeriodicInfo) - }) -_sym_db.RegisterMessage(PeriodicInfo) - -PeriodicInfos = _reflection.GeneratedProtocolMessageType('PeriodicInfos', (_message.Message,), { - 'DESCRIPTOR' : _PERIODICINFOS, - '__module__' : 'periodic_info_pb2' - # @@protoc_insertion_point(class_scope:PeriodicInfos) - }) -_sym_db.RegisterMessage(PeriodicInfos) - -if _descriptor._USE_C_DESCRIPTORS == False: - - DESCRIPTOR._options = None - _PERIODICINFO._serialized_start=23 - _PERIODICINFO._serialized_end=121 - _PERIODICINFOS._serialized_start=123 - _PERIODICINFOS._serialized_end=168 -# @@protoc_insertion_point(module_scope) diff --git a/protobuf/profile.proto b/protobuf/profile.proto index 98fa618..30499ca 100644 --- a/protobuf/profile.proto +++ b/protobuf/profile.proto @@ -1,242 +1,324 @@ 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 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, 0 + 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, 0 + optional fixed32 f26 = 26; //??? empty, 0 + optional fixed64 bike_frame_colour = 27; + optional fixed64 f28 = 28; //??? empty, 0 + optional fixed64 f29 = 29; //??? empty, 0 + optional fixed64 f30 = 30; //??? empty, 0 + optional fixed64 f31 = 31; //??? empty, 0 + optional fixed64 f32 = 32; //??? empty, 0 + 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, 0 + optional uint32 f54 = 54; //??? empty, 0 + optional uint32 age = 55; + optional fixed32 f56 = 56; //??? empty + optional uint32 f57 = 57; //??? empty, 0 + 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, 0 + optional uint32 f80 = 80; //??? empty, 0 + optional uint32 f81 = 81; //??? empty, 0 + 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, 1 + 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 when "RIDE"; increasing small value when "USE" (437, 438, 911, ..., 1229) + + 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; } message AchievementEntry { diff --git a/protobuf/profile_pb2.py b/protobuf/profile_pb2.py index 4d0d5b0..b54ead1 100644 --- a/protobuf/profile_pb2.py +++ b/protobuf/profile_pb2.py @@ -15,54 +15,104 @@ _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\"\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*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\"\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\"\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*;\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'] +_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'] +_PROPERTYCHANGE = DESCRIPTOR.message_types_by_name['PropertyChange'] +_ATTRIBUTE = DESCRIPTOR.message_types_by_name['Attribute'] _ACHIEVEMENTENTRY = DESCRIPTOR.message_types_by_name['AchievementEntry'] _ACHIEVEMENTS = DESCRIPTOR.message_types_by_name['Achievements'] -_PROFILE_PLAYERTYPE = _PROFILE.enum_types_by_name['PlayerType'] -_PROFILE_PROFILEENROLLEDPROGRAM = _PROFILE.enum_types_by_name['ProfileEnrolledProgram'] -_PROFILE_SPORT = _PROFILE.enum_types_by_name['Sport'] +_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'] +PlayerProfile = _reflection.GeneratedProtocolMessageType('PlayerProfile', (_message.Message,), { - 'ProfileSocialFacts' : _reflection.GeneratedProtocolMessageType('ProfileSocialFacts', (_message.Message,), { - 'DESCRIPTOR' : _PROFILE_PROFILESOCIALFACTS, + '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, @@ -78,12 +128,19 @@ 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) AchievementEntry = _reflection.GeneratedProtocolMessageType('AchievementEntry', (_message.Message,), { 'DESCRIPTOR' : _ACHIEVEMENTENTRY, @@ -102,38 +159,52 @@ _sym_db.RegisterMessage(Achievements) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None - _PROFILEFOLLOWSTATUS._serialized_start=3611 - _PROFILEFOLLOWSTATUS._serialized_end=3718 - _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 - _ACHIEVEMENTENTRY._serialized_start=3522 - _ACHIEVEMENTENTRY._serialized_end=3552 - _ACHIEVEMENTS._serialized_start=3554 - _ACHIEVEMENTS._serialized_end=3609 + _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 + _PLAYERPROFILE._serialized_start=18 + _PLAYERPROFILE._serialized_end=3815 + _PLAYERPROFILE_SOCIALFACTS._serialized_start=3114 + _PLAYERPROFILE_SOCIALFACTS._serialized_end=3409 + _PLAYERPROFILE_REMINDER._serialized_start=3412 + _PLAYERPROFILE_REMINDER._serialized_end=3568 + _PLAYERPROFILE_REMINDER_REMINDERPROPERTY._serialized_start=3514 + _PLAYERPROFILE_REMINDER_REMINDERPROPERTY._serialized_end=3568 + _PLAYERPROFILE_ENROLLEDPROGRAM._serialized_start=3570 + _PLAYERPROFILE_ENROLLEDPROGRAM._serialized_end=3694 + _PLAYERPROFILE_CYCLINGORGANIZATION._serialized_start=3696 + _PLAYERPROFILE_CYCLINGORGANIZATION._serialized_end=3815 + _PLAYERPROFILES._serialized_start=3817 + _PLAYERPROFILES._serialized_end=3867 + _PROFILEENTITLEMENT._serialized_start=3870 + _PROFILEENTITLEMENT._serialized_end=4686 + _PROFILEENTITLEMENT_ENTITLEMENTTYPE._serialized_start=4325 + _PROFILEENTITLEMENT_ENTITLEMENTTYPE._serialized_end=4401 + _PROFILEENTITLEMENT_PROFILEENTITLEMENTSTATUS._serialized_start=4404 + _PROFILEENTITLEMENT_PROFILEENTITLEMENTSTATUS._serialized_end=4549 + _PROFILEENTITLEMENT_PLATFORM._serialized_start=4552 + _PROFILEENTITLEMENT_PLATFORM._serialized_end=4686 + _SUBSCRIPTION._serialized_start=4689 + _SUBSCRIPTION._serialized_end=5021 + _SUBSCRIPTION_GATEWAY._serialized_start=4795 + _SUBSCRIPTION_GATEWAY._serialized_end=4830 + _SUBSCRIPTION_SUBSCRIPTIONSTATUS._serialized_start=4833 + _SUBSCRIPTION_SUBSCRIPTIONSTATUS._serialized_end=5021 + _PROPERTYCHANGE._serialized_start=5024 + _PROPERTYCHANGE._serialized_end=5174 + _PROPERTYCHANGE_ID._serialized_start=5128 + _PROPERTYCHANGE_ID._serialized_end=5174 + _ATTRIBUTE._serialized_start=5176 + _ATTRIBUTE._serialized_end=5264 + _ACHIEVEMENTENTRY._serialized_start=5266 + _ACHIEVEMENTENTRY._serialized_end=5296 + _ACHIEVEMENTS._serialized_start=5298 + _ACHIEVEMENTS._serialized_end=5353 # @@protoc_insertion_point(module_scope) diff --git a/protobuf/segment-result.proto b/protobuf/segment-result.proto index 0ad5d8a..ab92813 100644 --- a/protobuf/segment-result.proto +++ b/protobuf/segment-result.proto @@ -1,29 +1,34 @@ 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; + required string first_name = 7; //only first letter required string last_name = 8; 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; //only first letter of first_name } 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 98726b8..495eb96 100644 --- a/protobuf/udp-node-msgs.proto +++ b/protobuf/udp-node-msgs.proto @@ -1,128 +1,286 @@ 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 +} + +enum UserBikeAction { + UBA_ELBOW = 0; + UBA_WAVE = 1; + UBA_02 = 2; //BikeEntity::UpdateAnimation + UBA_RIDEON = 3; + UBA_HAMMER = 4; + UBA_NICE = 5; + UBA_BRING_IT = 6; + UBA_TOAST = 7; + UBA_BELL = 8; + UBA_HOLIDAY_WAVE = 9; //not sure +} + +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; //6, not r/w by game? + optional int64 wa_f16 = 16; //not r/w by game? stored as bool +} + +message PlayerLeftWorld { + required int64 player_id = 1; + optional int64 world_time = 2; // not sure + optional bool anotherLogin = 3; // user profile logged in twice -> one of them should be logged out + optional bool plw_f4 = 4; //true + repeated sint64 plw_f5 = 5; // [1056322864] +} + +/*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 dist_lat = 34; //= BikeEntity.field_F00 (=219.56387 and incr if moving: actual distance moved included lateral movement) 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; - repeated int64 subsSegments = 15; - repeated int64 unsSegments = 16; + 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 player_id = 1; //BikeManager::FindBikeWithNetworkID + optional float distance_covered = 2; //not sure + optional int32 prd_f3 = 3; //-> m_bikeEntity->field_9C8 +} + +message EventSubgroupPlacements { + optional int32 position = 1; //-> m_bikeEntity->field_9C0; UdpStatistics::registerFanViewLatestPlayerStateInfo + repeated PlayerRouteDistance player_rd1 = 2; + repeated PlayerRouteDistance player_rd2 = 3; + repeated PlayerRouteDistance eventRiderPosition = 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; //low-priority world time sync algo (not investigated yet, maybe deprecated) in WorldClockService::calculateOneLegLatency + // 6,7: absent repeated PlayerState states = 8; - repeated PlayerUpdate updates = 9; - optional int64 f11 = 11; - optional int64 latency = 17; + 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; + optional int64 cts_latency = 17; //high-priority world time sync algo in WorldClockService::calculateOneLegLatency optional int32 num_msgs = 18; optional int32 msgnum = 19; - repeated int64 ackSubsSegm = 30; + 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 3fbc980..845ecb1 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,92 @@ 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\"\xf7\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\x12\x14\n\x0csubsSegments\x18\x0f \x03(\x03\x12\x13\n\x0bunsSegments\x18\x10 \x03(\x03\"\xf1\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\x0f\n\x07latency\x18\x11 \x01(\x03\x12\x10\n\x08num_msgs\x18\x12 \x01(\x05\x12\x0e\n\x06msgnum\x18\x13 \x01(\x05\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\"\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\"n\n\x0fPlayerLeftWorld\x12\x11\n\tplayer_id\x18\x01 \x02(\x03\x12\x12\n\nworld_time\x18\x02 \x01(\x03\x12\x14\n\x0c\x61notherLogin\x18\x03 \x01(\x08\x12\x0e\n\x06plw_f4\x18\x04 \x01(\x08\x12\x0e\n\x06plw_f5\x18\x05 \x03(\x12\"\x93\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\x10\n\x08\x64ist_lat\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\"R\n\x13PlayerRouteDistance\x12\x11\n\tplayer_id\x18\x01 \x01(\x05\x12\x18\n\x10\x64istance_covered\x18\x02 \x01(\x02\x12\x0e\n\x06prd_f3\x18\x03 \x01(\x05\"\xac\x02\n\x17\x45ventSubgroupPlacements\x12\x10\n\x08position\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\x30\n\x12\x65ventRiderPosition\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\"\xc9\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\x13\n\x0b\x63ts_latency\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*\xac\x01\n\x0eUserBikeAction\x12\r\n\tUBA_ELBOW\x10\x00\x12\x0c\n\x08UBA_WAVE\x10\x01\x12\n\n\x06UBA_02\x10\x02\x12\x0e\n\nUBA_RIDEON\x10\x03\x12\x0e\n\nUBA_HAMMER\x10\x04\x12\x0c\n\x08UBA_NICE\x10\x05\x12\x10\n\x0cUBA_BRING_IT\x10\x06\x12\r\n\tUBA_TOAST\x10\x07\x12\x0c\n\x08UBA_BELL\x10\x08\x12\x14\n\x10UBA_HOLIDAY_WAVE\x10\t*\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) +_USERBIKEACTION = DESCRIPTOR.enum_types_by_name['UserBikeAction'] +UserBikeAction = enum_type_wrapper.EnumTypeWrapper(_USERBIKEACTION) +_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 +UBA_ELBOW = 0 +UBA_WAVE = 1 +UBA_02 = 2 +UBA_RIDEON = 3 +UBA_HAMMER = 4 +UBA_NICE = 5 +UBA_BRING_IT = 6 +UBA_TOAST = 7 +UBA_BELL = 8 +UBA_HOLIDAY_WAVE = 9 +UDP = 1 +TCP = 2 +_WORLDATTRIBUTE = DESCRIPTOR.message_types_by_name['WorldAttribute'] +_PLAYERLEFTWORLD = DESCRIPTOR.message_types_by_name['PlayerLeftWorld'] _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) + +PlayerLeftWorld = _reflection.GeneratedProtocolMessageType('PlayerLeftWorld', (_message.Message,), { + 'DESCRIPTOR' : _PLAYERLEFTWORLD, + '__module__' : 'udp_node_msgs_pb2' + # @@protoc_insertion_point(class_scope:PlayerLeftWorld) + }) +_sym_db.RegisterMessage(PlayerLeftWorld) + PlayerState = _reflection.GeneratedProtocolMessageType('PlayerState', (_message.Message,), { 'DESCRIPTOR' : _PLAYERSTATE, '__module__' : 'udp_node_msgs_pb2' @@ -41,6 +113,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 +190,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 +197,47 @@ 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=784 - _SERVERTOCLIENT._serialized_start=787 - _SERVERTOCLIENT._serialized_end=1028 - _GHOST._serialized_start=1030 - _GHOST._serialized_end=1086 - _GHOSTS._serialized_start=1088 - _GHOSTS._serialized_end=1120 - _PLAYERUPDATE._serialized_start=1123 - _PLAYERUPDATE._serialized_end=1326 - _CHATMESSAGE._serialized_start=1329 - _CHATMESSAGE._serialized_end=1507 - _RIDEON._serialized_start=1509 - _RIDEON._serialized_end=1614 - _SEGMENTCOMPLETE._serialized_start=1617 - _SEGMENTCOMPLETE._serialized_end=1913 + _ZOFFLINECONSTANTS._serialized_start=3527 + _ZOFFLINECONSTANTS._serialized_end=3559 + _WA_TYPE._serialized_start=3562 + _WA_TYPE._serialized_end=3964 + _USERBIKEACTION._serialized_start=3967 + _USERBIKEACTION._serialized_end=4139 + _IPPROTOCOL._serialized_start=4141 + _IPPROTOCOL._serialized_end=4171 + _WORLDATTRIBUTE._serialized_start=63 + _WORLDATTRIBUTE._serialized_end=373 + _PLAYERLEFTWORLD._serialized_start=375 + _PLAYERLEFTWORLD._serialized_end=485 + _PLAYERSTATE._serialized_start=488 + _PLAYERSTATE._serialized_end=1147 + _CLIENTTOSERVER._serialized_start=1150 + _CLIENTTOSERVER._serialized_end=1483 + _PLAYERSUMMARY._serialized_start=1485 + _PLAYERSUMMARY._serialized_end=1568 + _PLAYERSUMMARIES._serialized_start=1571 + _PLAYERSUMMARIES._serialized_end=1756 + _RELAYADDRESS._serialized_start=1758 + _RELAYADDRESS._serialized_end=1865 + _UDPCONFIG._serialized_start=1867 + _UDPCONFIG._serialized_end=1963 + _RELAYADDRESSESVOD._serialized_start=1965 + _RELAYADDRESSESVOD._serialized_end=2077 + _UDPCONFIGVOD._serialized_start=2080 + _UDPCONFIGVOD._serialized_end=2221 + _PLAYERROUTEDISTANCE._serialized_start=2223 + _PLAYERROUTEDISTANCE._serialized_end=2305 + _EVENTSUBGROUPPLACEMENTS._serialized_start=2308 + _EVENTSUBGROUPPLACEMENTS._serialized_end=2608 + _SERVERTOCLIENT._serialized_start=2611 + _SERVERTOCLIENT._serialized_end=3324 + _GHOST._serialized_start=3326 + _GHOST._serialized_end=3382 + _GHOSTS._serialized_start=3384 + _GHOSTS._serialized_end=3416 + _RIDEON._serialized_start=3418 + _RIDEON._serialized_end=3525 # @@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 old mode 100755 new mode 100644 index a5cf6fe..a179e2f --- a/standalone.py +++ b/standalone.py @@ -96,8 +96,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()) @@ -119,8 +119,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): @@ -230,9 +230,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) @@ -260,19 +261,22 @@ class TCPHandler(socketserver.BaseRequestHandler): def handle(self): self.data = self.request.recv(1024) if len(self.data) > 3 and self.data[3] != 0: + print("TCPHandler hello(0) expected, got %s" % self.data[3]) return - 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): @@ -280,41 +284,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()) @@ -323,34 +327,56 @@ class TCPHandler(socketserver.BaseRequestHandler): self.data = b'' try: self.data = self.request.recv(1024) + #print(self.data.hex()) i = 0 while i < len(self.data): size = int.from_bytes(self.data[i:i+2], "big") packet = self.data[i:i+size+2] + #print(packet.hex()) if len(packet) == size + 2 and packet[3] == 1: subscr = udp_node_msgs_pb2.ClientToServer() try: subscr.ParseFromString(packet[4:-4]) - except: - pass + #print(subscr) + except Exception as exc: + print('TCPHandler ParseFromString exception: %s' % repr(exc)) if subscr.subsSegments: msg1 = udp_node_msgs_pb2.ServerToClient() - msg1.f1 = 1 + msg1.server_realm = udp_node_msgs_pb2.ZofflineConstants.RealmID msg1.player_id = subscr.player_id msg1.world_time = zwift_offline.world_time() msg1.ackSubsSegm.extend(subscr.subsSegments) payload1 = msg1.SerializeToString() self.request.sendall(struct.pack('!h', len(payload1))) self.request.sendall(payload1) + #print('TCPHandler subscr: %s' % msg1.ackSubsSegm) i += size + 2 - except: - pass #timeout is ok here + except Exception as exc: + #print('TCPHandler exception: %s' % repr(exc)) #timeout is ok here + pass #Check every 5 seconds for new updates #tcpthreadevent.wait(timeout=5) # no more, we will use the request timeout now 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() @@ -368,7 +394,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() @@ -376,8 +402,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 @@ -388,7 +412,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: @@ -489,11 +514,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 @@ -519,7 +544,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 @@ -659,7 +685,7 @@ class UDPHandler(socketserver.BaseRequestHandler): if player != None: if len(message.states) > 9: message.world_time = zwift_offline.world_time() - message.latency = message.world_time - recv.world_time + message.cts_latency = message.world_time - recv.world_time socket.sendto(message.SerializeToString(), client_address) message.msgnum += 1 del message.states[:] @@ -668,7 +694,7 @@ class UDPHandler(socketserver.BaseRequestHandler): else: message.num_msgs = 1 message.world_time = zwift_offline.world_time() - message.latency = message.world_time - recv.world_time + message.cts_latency = message.world_time - recv.world_time socket.sendto(message.SerializeToString(), client_address) socketserver.ThreadingTCPServer.allow_reuse_address = True diff --git a/standalone.spec b/standalone.spec index cfbf958..98ecbcf 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 21ab2ff..9c49f1d 100644 --- a/zwift_offline.py +++ b/zwift_offline.py @@ -15,30 +15,33 @@ import threading import re import smtplib, ssl import requests -from copy import copy +import json +from copy import copy, deepcopy 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 import protobuf.per_session_info_pb2 as per_session_info_pb2 -import protobuf.periodic_info_pb2 as periodic_info_pb2 import protobuf.profile_pb2 as profile_pb2 import protobuf.segment_result_pb2 as segment_result_pb2 import protobuf.world_pb2 as world_pb2 @@ -129,7 +132,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 +157,7 @@ global_bots = {} global_ghosts = {} ghosts_enabled = {} player_update_queue = {} +zc_connect_queue = {} player_partial_profiles = {} save_ghost = None restarting = False @@ -183,7 +188,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: @@ -200,10 +206,23 @@ class AnonUser(User, AnonymousUserMixin, db.Model): return True class PartialProfile: + player_id = 0 first_name = '' last_name = '' country_code = 0 route = 0 + player_type = 'NORMAL' + male = True + imageSrc = '' + def to_json(self): + return {"countryCode": self.country_code, + "enrolledZwiftAcademy": False, #don't need + "firstName": self.first_name, + "id": self.player_id, + "imageSrc": self.imageSrc, + "lastName": self.last_name, + "male": self.male, + "playerType": self.player_type } class Online: total = 0 @@ -288,6 +307,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 @@ -300,18 +322,28 @@ 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.player_id = player_id + partial_profile.imageSrc = "https://us-or-rly101.zwift.com/download/%s/avatarLarge.jpg" % player_id 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 + partial_profile.player_type = profile_pb2.PlayerType.Name(jsf(profile, 'player_type', 1)) + partial_profile.male = profile.is_male + 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] @@ -322,6 +354,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 @@ -331,14 +365,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 @@ -464,13 +499,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 @@ -499,7 +552,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() @@ -524,7 +578,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) @@ -552,14 +607,15 @@ 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: file_path = os.path.join(profile_dir, 'zwift_credentials.txt') with open(file_path, 'w') as f: - f.write(username + '\n'); - f.write(password + '\n'); + f.write(username + '\n') + f.write(password + '\n') if credentials_key is not None: with open(file_path, 'rb') as fr: zwift_credentials = fr.read() @@ -568,9 +624,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) @@ -589,8 +647,8 @@ def garmin(username): try: file_path = os.path.join(STORAGE_DIR, str(current_user.player_id), 'garmin_credentials.txt') with open(file_path, 'w') as f: - f.write(username + '\n'); - f.write(password + '\n'); + f.write(username + '\n') + f.write(password + '\n') if credentials_key is not None: with open(file_path, 'rb') as fr: garmin_credentials = fr.read() @@ -599,7 +657,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) @@ -610,31 +669,33 @@ 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 enqueue_player_update(player_id, wa_bytes): + if not player_id in player_update_queue: + player_update_queue[player_id] = list() + player_update_queue[player_id].append(wa_bytes) 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 chat_message.countryCode = 0 player_update.payload = chat_message.SerializeToString() - - for recieving_player_id in online.keys(): - 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()) + player_update_s = player_update.SerializeToString() + for receiving_player_id in online.keys(): + enqueue_player_update(receiving_player_id, player_update_s) def send_restarting_message(): @@ -724,7 +785,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 @@ -748,13 +809,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 @@ -803,7 +872,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())) @@ -843,11 +913,109 @@ 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', 50)) + sport = request.args.get('sport', 'CYCLING') + events = get_events(eventCount, sport) + json_events = convert_events_to_json(events) + json_data = [] + for e in json_events: + json_data.append({"event": e}) + return jsonify({"data":json_data,"cursor":None}) + +@app.route('/api/campaign/profile/campaigns', 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): + profile = get_partial_profile(activity.player_id) + return {"id":activity.id,"profile":{"id":str(activity.player_id),"firstName":profile.first_name,"lastName":profile.last_name, \ + "imageSrc":profile.imageSrc,"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(): @@ -858,7 +1026,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: @@ -875,7 +1043,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 @@ -895,51 +1062,161 @@ 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 = [('Alpe Du Zwift Downhill', 1480439148), - ('Bologna TT', 2843604888), - ('Crit City', 947394567), - ('Crit City Reverse', 2875658892), - ('Gravel Mountain', 3687150686), - ('Gravel Mountain Reverse', 2956533021), - ('Neokyo Crit', 1127056801), - ('Ventop Downhill', 2891361683), - ('Watopia Waistband', 1064303857)] +def get_events(limit, sport): + events_list = [('Alpe Du Zwift Downhill', 1480439148, 6), + ('Bologna TT', 2843604888, 10), + ('Crit City', 947394567, 12), + ('Crit City Reverse', 2875658892, 12), + ('Gravel Mountain', 3687150686, 16), + ('Gravel Mountain Reverse', 2956533021, 16), + ('Neokyo Crit', 1127056801, 13), + ('Ventop Downhill', 2891361683, 14), + ('Watopia Waistband', 1064303857, 6)] event_id = 1000 + cnt = 0 events = events_pb2.Events() + const_delay_ms = 60000 + if sport == 'CYCLING': + sport = profile_pb2.Sport.CYCLING + else: + sport = profile_pb2.Sport.RUNNING + event_id = 1001 #to get sport back from id 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 = sport + event.lateJoinInMinutes = 30 + eventStart = int(get_time()) * 1000 + const_delay_ms + event.eventStart = eventStart + event.visible = True + event.overrideMapPreferences = False + event.invisibleToNonParticipants = False + event.description = "Auto-generated event" + event.distanceInMeters = 0 + event.laps = 0 + event.durationInSeconds = 0 + #event.rules_id = + #event.jerseyHash = + event.eventType = events_pb2.EventType.RACE + #event.e_f27 = 27; //<=4, ENUM? + #event.tags = 31; // semi-colon delimited tags + event.e_wtrl = False # WTRL (World Tactical Racing Leagues) + cats = ('A', 'B', 'C', 'D', 'E', 'F') + paceValues = ((4,15), (3,4), (2,3), (1,2), (0.1,1)) 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.registrationEndWT = world_time() + 60000 + #need good value#event_cat.registrationStart = eventStart - 30000 + #need good value#event_cat.registrationStartWT = world_time() + event_cat.registrationEnd = eventStart + event_cat.registrationEndWT = world_time() + const_delay_ms + #need good value#event_cat.lineUpStart = eventStart - 15000 + #need good value#event_cat.lineUpStartWT = world_time() + const_delay_ms - 15000 + #need good value#event_cat.lineUpEnd = eventStart + #need good value#event_cat.lineUpEndWT = world_time() + const_delay_ms + #need good value#event_cat.eventSubgroupStart = eventStart + #need good value#event_cat.eventSubgroupStartWT = world_time() + const_delay_ms event_cat.route_id = item[1] event_cat.startLocation = cat event_cat.label = cat + event_cat.lateJoinInMinutes = 30 + event_cat.name = "Cat.%s" % cats[cat - 1] + event_cat.description = "#zwiftoffline" + event_cat.course_id = event.course_id + event_cat.paceType = 1 #1 almost everywhere, 2 sometimes + event_cat.fromPaceValue = paceValues[cat - 1][0] + event_cat.toPaceValue = paceValues[cat - 1][1] + #event_cat.scode = 7; // ex: "PT3600S" + #event_cat.rules_id = 8; // 320 and others + event_cat.distanceInMeters = 0 + event_cat.laps = 0 + event_cat.durationInSeconds = 0 + #event_cat.jerseyHash = 36; // 493134166, tag672 + #event_cat.tags = 45; // tag746, semi-colon delimited tags eg: "fenced;3r;created_ryan;communityevent;no_kick_mode;timestamp=1603911177622" 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): + if event_id % 1 == 0: + sport = 'CYCLING' + else: + sport = 'RUNNING' + events = get_events(50, sport) + for e in events.events: + if e.id == event_id: + return jsonify(convert_event_to_json(e)) + return '', 200 -@app.route('/api/events/subgroups/signup/', methods=['POST']) -def api_events_subgroups_signup_id(event_id): - return '{"signedUp":true}', 200 +@app.route('/api/events/search', methods=['POST']) +def api_events_search(): + limit = int(request.args.get('limit')) + sport = request.args.get('sport', 'CYCLING') + events = get_events(limit, sport) + if request.headers['Accept'] == 'application/json': + return jsonify(convert_events_to_json(events)) + else: + return events.SerializeToString(), 200 +def create_event_wat(rel_id, wa_type, pe, dest_ids): + player_update = udp_node_msgs_pb2.WorldAttribute() + player_update.server_realm = udp_node_msgs_pb2.ZofflineConstants.RealmID + player_update.wa_type = wa_type + 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) + player_update.rel_id = current_user.player_id -@app.route('/api/events/subgroups/register/', methods=['POST']) -def api_events_subgroups_register_id(event_id): + pe.rel_id = rel_id + pe.player_id = current_user.player_id + #optional uint64 pje_f3/ple_f3 = 3; + player_update.payload = pe.SerializeToString() + player_update_s = player_update.SerializeToString() + + for receiving_player_id in dest_ids: + enqueue_player_update(receiving_player_id, player_update_s) + +@app.route('/api/events/subgroups/signup/', methods=['POST']) +@app.route('/api/events/signup/', methods=['DELETE']) +@jwt_to_session_cookie +@login_required +def api_events_subgroups_signup_id(rel_id): + if request.method == 'POST': + wa_type = udp_node_msgs_pb2.WA_TYPE.WAT_JOIN_E + pe = events_pb2.PlayerJoinedEvent() + ret = True + else: + wa_type = udp_node_msgs_pb2.WA_TYPE.WAT_LEFT_E + pe = events_pb2.PlayerLeftEvent() + ret = False + #empty request.data + create_event_wat(rel_id, wa_type, pe, online.keys()) + return jsonify({"signedUp":ret}) + +@app.route('/api/events/subgroups/register/', methods=['POST']) +def api_events_subgroups_register_id(ev_sg_id): return '{"registered":true}', 200 -@app.route('/api/events/subgroups/entrants/', methods=['GET']) -def api_events_subgroups_entrants_id(event_id): - return '', 200 +@app.route('/api/events/subgroups/entrants/', methods=['GET']) +def api_events_subgroups_entrants_id(ev_sg_id): + return '[]', 200 +@app.route('/api/events/subgroups/invited_ride_sweepers/', methods=['GET']) +def api_events_subgroups_invited_ride_sweepers_id(ev_sg_id): + return '[]', 200 + +@app.route('/api/events/subgroups/invited_ride_leaders/', methods=['GET']) +def api_events_subgroups_invited_ride_leaders_id(ev_sg_id): + return '[]', 200 @app.route('/relay/race/event_starting_line/', methods=['POST']) def relay_race_event_starting_line_id(event_id): @@ -949,7 +1226,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" @@ -968,30 +1245,83 @@ 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']) def api_zfiles_list(): return '', 200 - -# Probably don't need, haven't investigated -@app.route('/api/private_event/feed', methods=['GET', 'POST']) -def api_private_event_feed(): - return '', 200 - - # Disable telemetry (shuts up some errors in log) @app.route('/api/telemetry/config', methods=['GET']) def api_telemetry_config(): - return '{"isEnabled":false}' + return jsonify({"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(): - profile_id = current_user.player_id +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(profile_id, is_json): if MULTIPLAYER: profile_dir = '%s/%s' % (STORAGE_DIR, profile_id) else: @@ -1014,29 +1344,211 @@ 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: - 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: + 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(current_user.player_id, True) + else: + return do_api_profiles(current_user.player_id, False) + +@app.route('/api/profiles/me/', methods=['GET']) +@jwt_to_session_cookie +@login_required +def api_profiles_me_json(): + return do_api_profiles(current_user.player_id, True) + +@app.route('/api/profiles/', methods=['GET']) +@jwt_to_session_cookie +@login_required +def api_profiles_json(profile_id): + return do_api_profiles(profile_id, 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/profiles//followers', methods=['GET']) #?start=0&limit=200&include-follow-requests=false +@jwt_to_session_cookie +@login_required +def api_profiles_followers(m_player_id): + rows = db.session.execute(sqlalchemy.text("SELECT player_id,first_name,last_name FROM user")) + 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()) + #all users are following favourites of this user (temp decision for small crouds) + json_data_list.append({"id":0,"followerId":player_id,"followeeId":m_player_id,"status":"IS_FOLLOWING","isFolloweeFavoriteOfFollower":True, \ + "followerProfile":{"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}, \ + "followeeProfile":None}) + return jsonify(json_data_list) + +@app.route('/api/search/profiles/restricted', methods=['POST']) +@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']) @app.route('/api/profiles//in-game-fields', methods=['PUT']) @@ -1054,7 +1566,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 @@ -1062,6 +1574,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 @@ -1079,7 +1603,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() @@ -1100,10 +1624,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 @@ -1122,20 +1646,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) @@ -1155,51 +1679,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() @@ -1211,52 +1744,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() @@ -1268,8 +1809,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: @@ -1284,10 +1825,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 @@ -1318,7 +1859,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: @@ -1332,39 +1874,315 @@ def api_profiles_activities_id(player_id, activity_id): zwift_upload(player_id, activity) return response, 200 -@app.route('/api/profiles//activities/0/rideon', methods=['POST']) #activity_id Seem to always be 0, even when giving ride on to ppl with 30km+ +@app.route('/api/profiles//activities/0/rideon', methods=['POST']) #activity_id Seem to always be 0, even when giving ride on to ppl with 30km+ @jwt_to_session_cookie @login_required -def api_profiles_activities_rideon(recieving_player_id): +def api_profiles_activities_rideon(receiving_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(receiving_player_id) ride_on.firstName = profile.first_name ride_on.lastName = profile.last_name ride_on.countryCode = profile.country_code player_update.payload = ride_on.SerializeToString() - 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()) + enqueue_player_update(receiving_player_id, player_update.SerializeToString()) - receiver = get_partial_profile(recieving_player_id) + receiver = get_partial_profile(receiving_player_id) message = 'Ride on ' + receiver.first_name + ' ' + receiver.last_name + '!' discord.send_message(message, sending_player_id) return '{}', 200 +def stime_to_timestamp(stime): + utc_offset = datetime.datetime.fromtimestamp(0) - datetime.datetime.utcfromtimestamp(0) + return int((datetime.datetime.strptime(stime, '%Y-%m-%dT%H:%M:%SZ') + utc_offset).timestamp()) +glb_notifications = {} #player_id -> dictionary, todo: move to database +glb_cur_notif_id = 1 +def create_zca_notification(player_id, private_event, organizer): + global glb_cur_notif_id + if not player_id in glb_notifications.keys(): + glb_notifications[player_id] = {} + d = glb_notifications[player_id] + argString0 = json.dumps({"eventId":private_event['id'],"eventStartDate": \ + stime_to_timestamp(private_event['eventStart']), \ + "otherInviteeCount":len(private_event['invitedProfileIds'])}) + n = { "activity": None, "argLong0": 0, "argLong1": 0, "argString0": argString0, + "createdOn": str_timestamp(unix_time_millis(datetime.datetime.now())), + "fromProfile": { + "firstName": organizer["firstName"], + "id": organizer["id"], + "imageSrc": organizer["imageSrc"], + "imageSrcLarge": organizer["imageSrc"], + "lastName": organizer["lastName"], + "publicId": "283b140f-91d2-4882-bd8e-e4194ddf7128", #todo, hope not used + "socialFacts": { + "favoriteOfLoggedInPlayer": True, #todo + "followeeStatusOfLoggedInPlayer": "IS_FOLLOWING", #todo + "followerStatusOfLoggedInPlayer": "IS_FOLLOWING" #todo + } + }, + "id": glb_cur_notif_id, "lastModified": None, "read": False, "readDate": None, + "type": "PRIVATE_EVENT_INVITE" + } + d[glb_cur_notif_id] = n + glb_cur_notif_id += 1 + +@app.route('/api/notifications', methods=['GET']) +@jwt_to_session_cookie +@login_required +def api_notifications(): + ret_notifications = [] + if current_user.player_id in glb_notifications.keys(): + for n in glb_notifications[current_user.player_id].values(): + ret_notifications.append(n) + return jsonify(ret_notifications) + +@app.route('/api/notifications/', methods=['PUT']) +@jwt_to_session_cookie +@login_required +def api_notifications_put(notif_id): + n = glb_notifications[current_user.player_id][notif_id] + n["read"] = request.json['read'] + n["readDate"] = request.json['readDate'] + n["lastModified"] = n["readDate"] + return '', 204 + +glb_private_events = {} #todo: move to database +glb_cur_pe_id = 1 +@app.route('/api/private_event/', methods=['DELETE']) +@jwt_to_session_cookie +@login_required +def api_private_event_remove(meetup_id): + glb_private_events.pop(meetup_id) + for d in glb_notifications.values(): + if meetup_id in d.keys(): + d.pop(meetup_id) + return '', 200 + +def edit_private_event(player_id, meetup_id, decision): + if meetup_id in glb_private_events.keys(): + e = glb_private_events[meetup_id] + for i in e['eventInvites']: + if i['invitedProfile']['id'] == player_id: + i['status'] = decision + return '', 204 + +@app.route('/api/private_event//accept', methods=['PUT']) +@jwt_to_session_cookie +@login_required +def api_private_event_accept(meetup_id): + return edit_private_event(current_user.player_id, meetup_id, 'ACCEPTED') + +@app.route('/api/private_event//reject', methods=['PUT']) +@jwt_to_session_cookie +@login_required +def api_private_event_reject(meetup_id): + return edit_private_event(current_user.player_id, meetup_id, 'REJECTED') + +@app.route('/api/private_event/', methods=['PUT']) +@jwt_to_session_cookie +@login_required +def api_private_event_edit(meetup_id): + str_pe = request.stream.read() + json_pe = json.loads(str_pe) + org_json_pe = glb_private_events[meetup_id] + for f in ('culling', 'distanceInMeters', 'durationInSeconds', 'eventStart', 'invitedProfileIds', 'laps', 'routeId', 'rubberbanding', 'showResults', 'sport', 'workoutHash'): + org_json_pe[f] = json_pe[f] + org_json_pe['updateDate'] = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ") + for d in glb_notifications.values(): + if meetup_id in d.keys(): + n = d[meetup_id] + n['read'] = False + n['readDate'] = None + n['lastModified'] = org_json_pe['updateDate'] + return jsonify({"id":meetup_id}) + +def enqueue_wa_event_invites(player_id, wa): + for wat in (udp_node_msgs_pb2.WA_TYPE.WAT_EVENT, udp_node_msgs_pb2.WA_TYPE.WAT_INV_W): + wa.wa_type = wat + enqueue_player_update(player_id, wa.SerializeToString()) + +@app.route('/api/private_event', methods=['POST']) +@jwt_to_session_cookie +@login_required +def api_private_event_new(): #{"culling":true,"description":"mesg","distanceInMeters":13800.0,"durationInSeconds":0,"eventStart":"2022-03-17T16:27:00Z","invitedProfileIds":[4357549,4486967],"laps":0,"routeId":2474227587,"rubberbanding":true,"showResults":false,"sport":"CYCLING","workoutHash":0} + global glb_cur_pe_id + str_pe = request.stream.read() + json_pe = json.loads(str_pe) + pe_id = glb_cur_pe_id * 10 + 1 + glb_cur_pe_id += 1 + json_pe['id'] = pe_id + ev_sg_id = pe_id + 2 + json_pe['eventSubgroupId'] = ev_sg_id + json_pe['name'] = "Route #%s" % json_pe['routeId'] #todo: more readable + json_pe['acceptedTotalCount'] = len(json_pe['invitedProfileIds']) #todo: real count + json_pe['acceptedFolloweeCount'] = len(json_pe['invitedProfileIds']) + 1 #todo: real count + json_pe['invitedTotalCount'] = len(json_pe['invitedProfileIds']) + 1 + partial_profile = get_partial_profile(current_user.player_id) + json_pe['organizerProfileId'] = current_user.player_id + json_pe['organizerId'] = current_user.player_id + json_pe['startLocation'] = 1 #todo_pe + json_pe['allowsLateJoin'] = True #todo_pe + json_pe['organizerFirstName'] = partial_profile.first_name + json_pe['organizerLastName'] = partial_profile.last_name + json_pe['updateDate'] = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ") + json_pe['organizerImageUrl'] = "https://us-or-rly101.zwift.com/download/%s/avatarLarge.jpg" % current_user.player_id + eventInvites = [{"invitedProfile": partial_profile.to_json(), "status": "ACCEPTED"}] + create_event_wat(ev_sg_id, udp_node_msgs_pb2.WA_TYPE.WAT_JOIN_E, events_pb2.PlayerJoinedEvent(), online.keys()) + + pe = events_pb2.Event() + player_update = udp_node_msgs_pb2.WorldAttribute() + player_update.server_realm = udp_node_msgs_pb2.ZofflineConstants.RealmID + 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) + + pe.id = pe_id + pe.server_realm = udp_node_msgs_pb2.ZofflineConstants.RealmID + pe.name = json_pe['name'] + if 'description' in json_pe: + pe.description = json_pe['description'] + pe.eventStart = stime_to_timestamp(json_pe['eventStart'])*1000 + pe.distanceInMeters = json_pe['distanceInMeters'] + pe.laps = json_pe['laps'] + if 'imageUrl' in json_pe: + pe.imageUrl = json_pe['imageUrl'] + pe.durationInSeconds = json_pe['durationInSeconds'] + pe.route_id = json_pe['routeId'] + #{"rubberbanding":true,"showResults":false,"workoutHash":0} todo_pe + pe.visible = True + pe.jerseyHash = 0 + pe.sport = sport_from_str(json_pe['sport']) + #pe.uint64 e_f23 = 23; =0 + pe.eventType = events_pb2.EventType.EFONDO + if 'culling' in json_pe: + if json_pe['culling']: + pe.eventType = events_pb2.EventType.RACE + #pe.uint64 e_f25 = 25; =0 + pe.e_f27 = 2 #<=4, ENUM? saw = 2 + #pe.bool overrideMapPreferences = 28; =0 + #pe.bool invisibleToNonParticipants = 29; =0 todo_pe + pe.lateJoinInMinutes = 30 #todo_pe + #pe.course_id = 1 #todo_pe =f(json_pe['routeId']) ??? + player_update.payload = pe.SerializeToString() + enqueue_wa_event_invites(current_user.player_id, player_update) + + for peer_id in json_pe['invitedProfileIds']: + create_zca_notification(peer_id, json_pe, eventInvites[0]["invitedProfile"]) + player_update.rel_id = peer_id + enqueue_wa_event_invites(peer_id, player_update) + p_partial_profile = get_partial_profile(peer_id) + eventInvites.append({"invitedProfile": p_partial_profile.to_json(), "status": "PENDING"}) + json_pe['eventInvites'] = eventInvites + + glb_private_events[pe_id] = json_pe + return jsonify({"id":pe_id}), 201 + +def clone_and_append_social(player_id, private_event): + ret = deepcopy(private_event) + status = 'PENDING' + for i in ret['eventInvites']: + p = i['invitedProfile'] + #todo: strict social + if p['id'] == player_id: + p['socialFacts'] = {"followerStatusOfLoggedInPlayer":"SELF","isFavoriteOfLoggedInPlayer":False} + status = i['status'] + else: + p['socialFacts'] = {"followerStatusOfLoggedInPlayer":"IS_FOLLOWING","isFavoriteOfLoggedInPlayer":True} + ret['inviteStatus'] = status + return ret + +def jsonPrivateEventFeedToProtobuf(jfeed): + ret = events_pb2.PrivateEventFeedListProto() + for jpef in jfeed: + pef = ret.pef.add() + pef.event_id = jpef['id'] + pef.sport = sport_from_str(jpef['sport']) + pef.eventSubgroupStart = stime_to_timestamp(jpef['eventStart'])*1000 + pef.route_id = jpef['routeId'] + pef.durationInSeconds = jpef['durationInSeconds'] + pef.distanceInMeters = jpef['distanceInMeters'] + pef.answeredCount = 1 #todo + pef.invitedTotalCount = jpef['invitedTotalCount'] + pef.acceptedFolloweeCount = jpef['acceptedFolloweeCount'] + pef.acceptedTotalCount = jpef['acceptedTotalCount'] + pef.organizerImageUrl = jpef['organizerImageUrl'] + pef.organizerProfileId = jpef['organizerProfileId'] + pef.organizerFirstName = jpef['organizerFirstName'] + pef.organizerLastName = jpef['organizerLastName'] + pef.updateDate = stime_to_timestamp(jpef['updateDate'])*1000 + pef.subgroupId = jpef['eventSubgroupId'] + pef.laps = jpef['laps'] + pef.rubberbanding = jpef['rubberbanding'] + return ret + +@app.route('/api/private_event/feed', methods=['GET']) +@jwt_to_session_cookie +@login_required +def api_private_event_feed(): + ret = [] + for pe in glb_private_events.values(): + ret.append(clone_and_append_social(current_user.player_id, pe)) + if(request.headers['Accept'] == 'application/json'): + return jsonify(ret) + return jsonPrivateEventFeedToProtobuf(ret).SerializeToString(), 200 + +def jsonPrivateEventToProtobuf(je): + ret = events_pb2.PrivateEventProto() + ret.id = je['id'] + ret.sport = sport_from_str(je['sport']) + ret.eventStart = stime_to_timestamp(je['eventStart'])*1000 + ret.routeId = je['routeId'] + ret.startLocation = je['startLocation'] + ret.durationInSeconds = je['durationInSeconds'] + ret.distanceInMeters = je['distanceInMeters'] + if 'description' in je: + ret.description = je['description'] + ret.workoutHash = je['workoutHash'] + ret.organizerId = je['organizerProfileId'] + for jinv in je['eventInvites']: + jp = jinv['invitedProfile'] + inv = ret.eventInvites.add() + inv.profile.player_id = jp['id'] + inv.profile.firstName = jp['firstName'] + inv.profile.lastName = jp['lastName'] + inv.profile.imageSrc = jp['imageSrc'] + inv.profile.enrolledZwiftAcademy = jp['enrolledZwiftAcademy'] + inv.profile.male = jp['male'] + inv.profile.player_type = profile_pb2.PlayerType.Value(jp['playerType']) + inv.status = events_pb2.EventInviteStatus.Value(jinv['status']) + ret.showResults = je['showResults'] + ret.laps = je['laps'] + ret.rubberbanding = je['rubberbanding'] + return ret + +@app.route('/api/private_event/', methods=['GET']) +@jwt_to_session_cookie +@login_required +def api_private_event_id(event_id): + ret = clone_and_append_social(current_user.player_id, glb_private_events[event_id]) + if(request.headers['Accept'] == 'application/json'): + return jsonify(ret) + return jsonPrivateEventToProtobuf(ret).SerializeToString(), 200 + +@app.route('/api/private_event/entitlement', methods=['GET']) +def api_private_event_entitlement(): + return jsonify({"entitled": True}) + +@app.route('/api/profiles//campaigns/otm2020', methods=['GET']) @app.route('/api/profiles//followees', methods=['GET']) def api_profiles_followees(player_id): return '', 200 @@ -1388,17 +2206,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: @@ -1418,12 +2237,97 @@ 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): + if ts == None: + return None + else: + 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 str_timestamp_json(ts): + if ts == 0: + return None + else: + return str_timestamp(ts) + +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_json(goal.created_on), +"periodEndDate":str_timestamp_json(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 @@ -1434,34 +2338,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']) @@ -1479,13 +2382,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 @@ -1496,109 +2399,108 @@ 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) - for recieving_player_id in online.keys(): + 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 == 5 or player_update.type == 105: - recieving_player = online[recieving_player_id] + 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: + 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] #Check that players are on same course and close to each other - if is_nearby(sending_player, recieving_player): + 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] #Check that players are on same course - if get_course(sending_player) == get_course(recieving_player) or recieving_player.watchingRiderId == sending_player_id: + if get_course(sending_player) == get_course(receiving_player) or receiving_player.watchingRiderId == sending_player_id: should_receive = True #Other PlayerUpdate, send to all else: should_receive = True if should_receive: - 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() + enqueue_player_update(receiving_player_id, player_update.SerializeToString()) + 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] @@ -1611,31 +2513,105 @@ 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 convert_event_to_json(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":str_timestamp_json(event_cat.registrationStart),"registrationEnd":str_timestamp_json(event_cat.registrationEnd),"lineUpStart":str_timestamp_json(event_cat.lineUpStart), \ +"lineUpEnd":str_timestamp_json(event_cat.lineUpEnd),"eventSubgroupStart":str_timestamp_json(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":str_timestamp_json(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":events_pb2._EVENTTYPE.values_by_number[int(event.eventType)].name, \ +"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 = convert_event_to_json(e) + json_events.append(json_event) + return json_events + +def transformPrivateEvents(player_id, max_count, status): + ret = [] + if max_count > 0: + for e in glb_private_events.values(): + for i in e['eventInvites']: + if i['invitedProfile']['id'] == player_id: + if i['status'] == status: + e_clone = deepcopy(e) + e_clone['inviteStatus'] = status + ret.append(e_clone) + if len(ret) >= max_count: + return ret + return ret + +#todo: followingCount=3&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, 'CYCLING') #runners, sorry! + json_events = convert_events_to_json(events) + pendingEventInviteCount = int(request.args.get('pendingEventInviteCount')) + ppeFeed = transformPrivateEvents(current_user.player_id, pendingEventInviteCount, 'PENDING') + acceptedEventInviteCount = int(request.args.get('acceptedEventInviteCount')) + apeFeed = transformPrivateEvents(current_user.player_id, acceptedEventInviteCount, 'ACCEPTED') + return jsonify({"events":json_events,"goals":json_goals,"activities":json_activities,"pendingPrivateEventFeed":ppeFeed,"acceptedPrivateEventFeed":apeFeed,"hasFolloweesToRideOn":False, \ + "worldName":"MAKURIISLANDS","playerCount": len(online),"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(world_id): +@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() @@ -1648,11 +2624,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}]' @@ -1668,46 +2643,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: @@ -1716,33 +2692,15 @@ def relay_worlds_attributes(): else: should_receive = True if should_receive: - if not receiving_player_id in player_update_queue: - player_update_queue[receiving_player_id] = list() - player_update_queue[receiving_player_id].append(player_update.SerializeToString()) + enqueue_player_update(receiving_player_id, 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 -@app.route('/relay/periodic-info', methods=['GET']) -def relay_periodic_info(): - infos = periodic_info_pb2.PeriodicInfos() - info = infos.infos.add() - if request.remote_addr == '127.0.0.1': # to avoid needing hairpinning - info.game_server_ip = "127.0.0.1" - else: - info.game_server_ip = server_ip - info.f2 = 3022 - info.f3 = 10 - info.f4 = 60 - info.f5 = 30 - info.f6 = 3 - return infos.SerializeToString(), 200 - - def add_segment_results(segment_id, player_id, only_best, from_date, to_date, results): where_stmt = ("WHERE segment_id = '%s'" % segment_id) rows = None @@ -1783,13 +2741,13 @@ def handle_segment_results(request): insert_protobuf_into_db('segment_result', result) # Previously done in /relay/worlds/attributes - player_update = udp_node_msgs_pb2.PlayerUpdate() - player_update.f2 = 1 - player_update.type = 105 + 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_SR player_update.payload = data - player_update.world_time1 = world_time() - player_update.world_time2 = world_time() + 60000 - player_update.f14 = int(get_utc_time() * 1000000) + player_update.world_time_born = world_time() + player_update.world_time_expire = world_time() + 60000 + player_update.timestamp = int(get_utc_time() * 1000000) sending_player_id = result.player_id if sending_player_id in online: sending_player = online[sending_player_id] @@ -1797,11 +2755,9 @@ def handle_segment_results(request): if receiving_player_id != sending_player_id: receiving_player = online[receiving_player_id] if get_course(sending_player) == get_course(receiving_player) or receiving_player.watchingRiderId == sending_player_id: - if not receiving_player_id in player_update_queue: - player_update_queue[receiving_player_id] = list() - player_update_queue[receiving_player_id].append(player_update.SerializeToString()) + enqueue_player_update(receiving_player_id, player_update.SerializeToString()) - return '{"id": %ld}' % result.id, 200 + return {"id": result.id} # request.method == GET # world_id = int(request.args.get('world_id')) @@ -1816,15 +2772,10 @@ 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: - #Add players results - add_segment_results(segment_id, player_id, only_best, from_date, to_date, results) - else: - #Top 100 results, player_id = None - add_segment_results(segment_id, player_id, only_best, from_date, to_date, results) + add_segment_results(segment_id, player_id, only_best, from_date, to_date, results) return results.SerializeToString(), 200 @@ -1848,7 +2799,7 @@ def api_segment_results(): @app.route('/live-segment-results-service/leaders', methods=['GET']) def live_segment_results_service_leaders(): results = segment_result_pb2.SegmentResults() - results.world_id = 0 + results.server_realm = 0 results.segment_id = 0 rows = db.session.execute(sqlalchemy.text("""SELECT s1.* FROM segment_result s1 JOIN (SELECT s.player_id, s.segment_id, MIN(CAST(s.elapsed_ms AS INTEGER)) AS min_time @@ -1867,7 +2818,7 @@ def live_segment_results_service_leaders(): def live_segment_results_service_leaderboard_segment_id(segment_id): id = int(segment_id) & 0xffffffffffffffff results = segment_result_pb2.SegmentResults() - results.world_id = 0 + results.server_realm = 0 results.segment_id = id rows = db.session.execute(sqlalchemy.text("""SELECT s1.* FROM segment_result s1 JOIN (SELECT s.player_id, MIN(CAST(s.elapsed_ms AS INTEGER)) AS min_time @@ -1882,8 +2833,8 @@ def live_segment_results_service_leaderboard_segment_id(segment_id): return results.SerializeToString(), 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() @@ -1901,7 +2852,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: @@ -1940,8 +2891,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 @@ -2046,7 +2997,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']) @@ -2069,19 +3020,30 @@ 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']) + if 'session_cookie' in token: + return jsonify(fake_jwt_with_session_cookie(token['session_cookie'])) + else: + return '', 401 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) + session.clear() + return '', 204 @app.route("/start-zwift" , methods=['POST']) @login_required @@ -2132,20 +3094,18 @@ 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.Profile() + profile = profile_pb2.PlayerProfile() profile.ParseFromString(fd.read()) - if len(profile.f33) > 0x150 and profile.f33[0x108] == 2: - return profile.f33[0x110:0x110+0x40] + if len(profile.saved_game) > 0x150 and profile.saved_game[0x108] == 2: #checking 2 from 0x10000002: achiev_badges2_40 + return profile.saved_game[0x110:0x110+0x40] #0x110 = accessories1_100 + 2x8-byte headers else: return b'' - @app.route('/achievement/loadPlayerAchievements', methods=['GET']) @jwt_to_session_cookie @login_required