diff --git a/README.md b/README.md index ce15b5a5..97086cc8 100644 --- a/README.md +++ b/README.md @@ -371,6 +371,10 @@ To enable support for multiple users perform the steps below: * You can teleport to a bookmark using the teleport icon on the action bar. +### All-time leaderboards + +To enable all-time leaderboards (override 60 minutes live results and 90 days personal records), create a file ``all_time_leaderboards.txt`` in the ``storage`` directory. + ## Community Discord server and Strava club Please join the community supported [Discord](https://discord.gg/GMdn8F8) server and [Strava](https://www.strava.com/clubs/zoffline) club. diff --git a/zwift_offline.py b/zwift_offline.py index 2c39b2f5..54892968 100644 --- a/zwift_offline.py +++ b/zwift_offline.py @@ -115,6 +115,7 @@ def make_dir(name): if os.path.exists(GHOST_PROFILE_FILE): with open(GHOST_PROFILE_FILE) as f: GHOST_PROFILE = json.load(f) +ALL_TIME_LEADERBOARDS = os.path.exists("%s/all_time_leaderboards.txt" % STORAGE_DIR) MULTIPLAYER = os.path.exists("%s/multiplayer.txt" % STORAGE_DIR) if MULTIPLAYER: if not make_dir(LOGS_DIR): @@ -2132,7 +2133,7 @@ def player_playbacks_player_playbacks(player_id, segment_id, option): pb_type = playback_pb2.PlaybackType.Value(request.args.get('type')) query = "SELECT * FROM playback WHERE player_id = :p AND segment_id = :s AND type = :t" args = {"p": player_id, "s": segment_id, "t": pb_type} - if after != '18446744065933551616': + if after != '18446744065933551616' and not ALL_TIME_LEADERBOARDS: query += " AND world_time > :a" args.update({"a": after}) if before != '0': @@ -3345,13 +3346,13 @@ def api_personal_records_my_records(): where_stmt = "WHERE segment_id = :s AND player_id = :p" args = {"s": segment_id, "p": current_user.player_id} - if from_date: + if from_date and not ALL_TIME_LEADERBOARDS: where_stmt += " AND strftime('%s', finish_time_str) > strftime('%s', :f)" args.update({"f": from_date}) if to_date: where_stmt += " AND strftime('%s', finish_time_str) < strftime('%s', :t)" args.update({"t": to_date}) - rows = db.session.execute(sqlalchemy.text("SELECT * FROM segment_result %s" % where_stmt), args).mappings() + rows = db.session.execute(sqlalchemy.text("SELECT * FROM segment_result %s ORDER BY elapsed_ms LIMIT 100" % where_stmt), args).mappings() for row in rows: result = results.segment_results.add() row_to_protobuf(row, result, ['server_realm', 'course_id', 'segment_id', 'event_subgroup_id', 'finish_time_str', 'f14', 'time', 'player_type', 'f22', 'f23']) @@ -3498,20 +3499,31 @@ def api_route_results_completion_stats_all(): return jsonify(response) +def add_segment_results(results, rows): + for row in rows: + result = results.segment_results.add() + row_to_protobuf(row, result, ['f14', 'time', 'player_type', 'f22']) + if ALL_TIME_LEADERBOARDS and result.world_time <= world_time() - 60 * 60 * 1000: + result.player_id += 100000 # avoid taking the jersey + result.world_time = world_time() # otherwise client filters it out + @app.route('/live-segment-results-service/leaders', methods=['GET']) def live_segment_results_service_leaders(): results = segment_result_pb2.SegmentResults() results.server_realm = 0 results.segment_id = 0 + where_stmt = "" + args = {} + if not ALL_TIME_LEADERBOARDS: + where_stmt = "WHERE world_time > :w" + args = {"w": world_time() - 60 * 60 * 1000} stmt = sqlalchemy.text("""SELECT s1.* FROM segment_result s1 JOIN (SELECT s.player_id, s.segment_id, MIN(s.elapsed_ms) AS min_time - FROM segment_result s WHERE world_time > :w GROUP BY s.player_id, s.segment_id) s2 - ON s2.player_id = s1.player_id AND s2.min_time = s1.elapsed_ms - GROUP BY s1.player_id, s1.elapsed_ms ORDER BY s1.segment_id, s1.elapsed_ms LIMIT 1000""") - rows = db.session.execute(stmt, {"w": world_time()-60*60*1000}).mappings() - for row in rows: - result = results.segment_results.add() - row_to_protobuf(row, result, ['f14', 'time', 'player_type', 'f22']) + FROM segment_result s %s GROUP BY s.player_id, s.segment_id) s2 + ON s2.player_id = s1.player_id AND s2.min_time = s1.elapsed_ms + GROUP BY s1.player_id, s1.elapsed_ms ORDER BY s1.segment_id, s1.elapsed_ms LIMIT 100""" % where_stmt) + rows = db.session.execute(stmt, args).mappings() + add_segment_results(results, rows) return results.SerializeToString(), 200 @@ -3521,15 +3533,18 @@ def live_segment_results_service_leaderboard_segment_id(segment_id): results = segment_result_pb2.SegmentResults() results.server_realm = 0 results.segment_id = segment_id + where_stmt = "WHERE segment_id = :s" + args = {"s": segment_id} + if not ALL_TIME_LEADERBOARDS: + where_stmt += " AND world_time > :w" + args.update({"w": world_time() - 60 * 60 * 1000}) stmt = sqlalchemy.text("""SELECT s1.* FROM segment_result s1 JOIN (SELECT s.player_id, MIN(s.elapsed_ms) AS min_time - FROM segment_result s WHERE segment_id = :s AND world_time > :w GROUP BY s.player_id) s2 - ON s2.player_id = s1.player_id AND s2.min_time = s1.elapsed_ms - GROUP BY s1.player_id, s1.elapsed_ms ORDER BY s1.elapsed_ms LIMIT 1000""") - rows = db.session.execute(stmt, {"s": segment_id, "w": world_time()-60*60*1000}).mappings() - for row in rows: - result = results.segment_results.add() - row_to_protobuf(row, result, ['f14', 'time', 'player_type', 'f22']) + FROM segment_result s %s GROUP BY s.player_id) s2 + ON s2.player_id = s1.player_id AND s2.min_time = s1.elapsed_ms + GROUP BY s1.player_id, s1.elapsed_ms ORDER BY s1.elapsed_ms LIMIT 100""" % where_stmt) + rows = db.session.execute(stmt, args).mappings() + add_segment_results(results, rows) return results.SerializeToString(), 200